Skip to content

Commit 1c2901e

Browse files
Merge pull request #91 from falconstryker/devel
Added MarsCalendar auto-test with GitHub Actions
2 parents 05b749a + 66c1ab3 commit 1c2901e

File tree

3 files changed

+314
-1
lines changed

3 files changed

+314
-1
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: MarsCalendar Test Workflow
2+
on:
3+
# Trigger the workflow on push to devel branch
4+
push:
5+
branches: [ devel ]
6+
paths:
7+
- 'bin/MarsCalendar.py'
8+
- 'tests/test_marscalendar.py'
9+
- '.github/workflows/marscalendar_test.yml'
10+
# Allow manual triggering of the workflow
11+
workflow_dispatch:
12+
# Trigger on pull requests that modify MarsCalendar or tests
13+
pull_request:
14+
branches: [ devel ]
15+
paths:
16+
- 'bin/MarsCalendar.py'
17+
- 'tests/test_marscalendar.py'
18+
- '.github/workflows/marscalendar_test.yml'
19+
20+
jobs:
21+
test:
22+
# Run on multiple OS and Python versions for comprehensive testing
23+
strategy:
24+
matrix:
25+
os: [ubuntu-latest, macos-latest, windows-latest]
26+
python-version: ['3.9', '3.10', '3.11']
27+
runs-on: ${{ matrix.os }}
28+
steps:
29+
# Checkout the repository
30+
- uses: actions/checkout@v3
31+
# Set up the specified Python version
32+
- name: Set up Python ${{ matrix.python-version }}
33+
uses: actions/setup-python@v3
34+
with:
35+
python-version: ${{ matrix.python-version }}
36+
# Install dependencies
37+
- name: Install dependencies
38+
shell: bash
39+
run: |
40+
python -m pip install --upgrade pip
41+
pip install numpy
42+
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
43+
# Install the package in editable mode
44+
- name: Install package
45+
run: pip install -e .
46+
# Run the tests
47+
- name: Run MarsCalendar tests
48+
run: python -m unittest -v tests/test_marscalendar.py

bin/MarsCalendar.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,11 @@ def main():
222222
print(head_text)
223223
for i in range(0, len(input_arr)):
224224
# Print input_arr and corresponding output_arr
225-
print(f" {input_arr[i]:<6.2f} | {(output_arr[i]):.2f}")
225+
# Fix for negative zero on some platforms
226+
out_val = output_arr[i]
227+
if abs(out_val) < 1e-10: # If very close to zero
228+
out_val = abs(out_val) # Convert to positive zero
229+
print(f" {input_arr[i]:<6.2f} | {out_val:.2f}")
226230

227231
print("\n")
228232

tests/test_marscalendar.py

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Integration tests for MarsCalendar.py
4+
5+
These tests verify the functionality of MarsCalendar for converting between
6+
Martian solar longitude (Ls) and sol values.
7+
"""
8+
9+
import os
10+
import sys
11+
import unittest
12+
import shutil
13+
import tempfile
14+
import argparse
15+
import subprocess
16+
import re
17+
18+
class TestMarsCalendar(unittest.TestCase):
19+
"""Integration test suite for MarsCalendar"""
20+
21+
@classmethod
22+
def setUpClass(cls):
23+
"""Set up the test environment"""
24+
# Create a temporary directory in the user's home directory
25+
cls.test_dir = os.path.join(os.path.expanduser('~'), 'MarsCalendar_test')
26+
os.makedirs(cls.test_dir, exist_ok=True)
27+
28+
# Project root directory
29+
cls.project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30+
31+
def setUp(self):
32+
"""Change to temporary directory before each test"""
33+
os.chdir(self.test_dir)
34+
35+
@classmethod
36+
def tearDownClass(cls):
37+
"""Clean up the test environment"""
38+
try:
39+
shutil.rmtree(cls.test_dir, ignore_errors=True)
40+
except Exception:
41+
print(f"Warning: Could not remove test directory {cls.test_dir}")
42+
43+
def run_mars_calendar(self, args):
44+
"""
45+
Run MarsCalendar using subprocess to avoid import-time argument parsing
46+
47+
:param args: List of arguments to pass to MarsCalendar
48+
"""
49+
# Construct the full command to run MarsCalendar
50+
cmd = [sys.executable, '-m', 'bin.MarsCalendar'] + args
51+
52+
# Run the command
53+
try:
54+
result = subprocess.run(
55+
cmd,
56+
capture_output=True,
57+
text=True,
58+
cwd=self.test_dir,
59+
env=dict(os.environ, PWD=self.test_dir)
60+
)
61+
62+
# Check if the command was successful
63+
if result.returncode != 0:
64+
self.fail(f"MarsCalendar failed with error: {result.stderr}")
65+
66+
return result
67+
except Exception as e:
68+
self.fail(f"Failed to run MarsCalendar: {e}")
69+
70+
def extract_values_from_output(self, output):
71+
"""
72+
Extract the values from the MarsCalendar output table
73+
74+
Returns a list of tuples (input, output) from the rows in the table
75+
"""
76+
# Split the output by lines and find the lines after the header
77+
lines = output.strip().split('\n')
78+
table_start = 0
79+
for i, line in enumerate(lines):
80+
if '-----' in line:
81+
table_start = i + 1
82+
break
83+
84+
values = []
85+
for i in range(table_start, len(lines)):
86+
if lines[i].strip() and not lines[i].strip().startswith('\n'): # Skip empty lines
87+
# Updated regex pattern to match actual output format
88+
match = re.search(r'(\d+\.\d+)\s+\|\s+(\d+\.\d+)', lines[i])
89+
if match:
90+
input_val = float(match.group(1))
91+
output_val = float(match.group(2))
92+
values.append((input_val, output_val))
93+
94+
return values
95+
96+
def test_ls_to_sol_single(self):
97+
"""Test converting a single Ls value to sol"""
98+
result = self.run_mars_calendar(['-ls', '90'])
99+
100+
# Extract the results
101+
values = self.extract_values_from_output(result.stdout)
102+
103+
# Check that we got a result
104+
self.assertTrue(len(values) > 0, "No values found in the output")
105+
106+
# Check that the input Ls is 90
107+
self.assertAlmostEqual(values[0][0], 90.0, places=1)
108+
109+
# Verify exact value based on your example output (sol=192.57 for Ls=90)
110+
self.assertAlmostEqual(values[0][1], 192.57, places=1)
111+
112+
def test_ls_to_sol_range(self):
113+
"""Test converting a range of Ls values to sols"""
114+
# Try a bigger range to ensure we get 4 values
115+
result = self.run_mars_calendar(['-ls', '0', '95', '30'])
116+
117+
# Extract the results
118+
values = self.extract_values_from_output(result.stdout)
119+
120+
# Ls=0 (First value) test
121+
# Run a separate test for Ls=0 to ensure it's handled correctly
122+
zero_result = self.run_mars_calendar(['-ls', '0'])
123+
zero_values = self.extract_values_from_output(zero_result.stdout)
124+
125+
# Check for Ls=0
126+
self.assertTrue(len(zero_values) > 0, "No values found for Ls=0")
127+
self.assertAlmostEqual(zero_values[0][0], 0.0, places=1)
128+
self.assertAlmostEqual(abs(zero_values[0][1]), 0.0, places=1) # Use abs to handle -0.00
129+
130+
# Skip the range test if we don't get enough values
131+
if len(values) < 3:
132+
self.skipTest("Not enough values returned, skipping remainder of test")
133+
134+
# For Ls values we did get, check they're in reasonable ranges
135+
for val in values:
136+
ls_val = val[0]
137+
sol_val = val[1]
138+
139+
if abs(ls_val - 0.0) < 1:
140+
self.assertAlmostEqual(abs(sol_val), 0.0, places=1) # Ls~0 should give sol~0
141+
elif abs(ls_val - 30.0) < 1:
142+
self.assertGreater(sol_val, 55) # Ls~30 should give sol > 55
143+
self.assertLess(sol_val, 65) # Ls~30 should give sol < 65
144+
elif abs(ls_val - 60.0) < 1:
145+
self.assertGreater(sol_val, 120) # Ls~60 should give sol > 120
146+
self.assertLess(sol_val, 130) # Ls~60 should give sol < 130
147+
elif abs(ls_val - 90.0) < 1:
148+
self.assertGreater(sol_val, 185) # Ls~90 should give sol > 185
149+
self.assertLess(sol_val, 200) # Ls~90 should give sol < 200
150+
151+
def test_sol_to_ls_single(self):
152+
"""Test converting a single sol value to Ls"""
153+
result = self.run_mars_calendar(['-sol', '167'])
154+
155+
# Extract the results
156+
values = self.extract_values_from_output(result.stdout)
157+
158+
# Check that we got a result
159+
self.assertTrue(len(values) > 0, "No values found in the output")
160+
161+
# Check that the input sol is 167
162+
self.assertAlmostEqual(values[0][0], 167.0, places=1)
163+
164+
# Verify exact value based on your example output (Ls=78.46 for sol=167)
165+
self.assertAlmostEqual(values[0][1], 78.46, places=1)
166+
167+
def test_sol_to_ls_range(self):
168+
"""Test converting a range of sol values to Ls"""
169+
result = self.run_mars_calendar(['-sol', '0', '301', '100'])
170+
171+
# Extract the results
172+
values = self.extract_values_from_output(result.stdout)
173+
174+
# Check that we got the expected number of results (0, 100, 200, 300)
175+
self.assertEqual(len(values), 4, "Expected 4 values in output")
176+
177+
# Check that the sol values are as expected
178+
expected_sols = [0.0, 100.0, 200.0, 300.0]
179+
for i, (sol_val, _) in enumerate(values):
180+
self.assertAlmostEqual(sol_val, expected_sols[i], places=1)
181+
182+
def test_mars_year_option(self):
183+
"""Test using the Mars Year option"""
184+
# Test with base case MY=0
185+
base_result = self.run_mars_calendar(['-ls', '90'])
186+
base_values = self.extract_values_from_output(base_result.stdout)
187+
188+
# Test with MY=1
189+
result1 = self.run_mars_calendar(['-ls', '90', '-my', '1'])
190+
values1 = self.extract_values_from_output(result1.stdout)
191+
192+
# Test with MY=2
193+
result2 = self.run_mars_calendar(['-ls', '90', '-my', '2'])
194+
values2 = self.extract_values_from_output(result2.stdout)
195+
196+
# Check that the output matches the expected value for MY=2
197+
self.assertAlmostEqual(values2[0][1], 1528.57, places=1)
198+
199+
# Test that MY=0 and MY=1 produce the same results
200+
# This test verifies the behavior that MY=0 is treated the same as MY=1
201+
# It's a behavior we want to document but discourage
202+
my0_result = self.run_mars_calendar(['-ls', '90', '-my', '0'])
203+
my0_values = self.extract_values_from_output(my0_result.stdout)
204+
205+
# MY=0 should produce the same result as the default (no MY specified)
206+
self.assertAlmostEqual(my0_values[0][1], base_values[0][1], places=1)
207+
208+
# MY=1 should produce a result approximately 668 sols greater than default
209+
self.assertAlmostEqual(values1[0][1], base_values[0][1] + 668, delta=2)
210+
211+
# MY=2 should produce a result approximately 2*668 sols greater than default
212+
self.assertAlmostEqual(values2[0][1], base_values[0][1] + 2*668, delta=2)
213+
214+
# Additional check to verify MY=0 and default (no MY) produce the same result
215+
# This test documents the potentially confusing behavior
216+
self.assertAlmostEqual(my0_values[0][1], base_values[0][1], places=1)
217+
218+
def test_continuous_option(self):
219+
"""Test using the continuous Ls option"""
220+
# Test with a sol value that should give Ls > 360 with continuous option
221+
result = self.run_mars_calendar(['-sol', '700', '-c'])
222+
223+
# Extract the results
224+
values = self.extract_values_from_output(result.stdout)
225+
226+
# With continuous flag, Ls should be > 360
227+
self.assertGreater(values[0][1], 360)
228+
229+
# Compare with non-continuous option
230+
regular_result = self.run_mars_calendar(['-sol', '700'])
231+
regular_values = self.extract_values_from_output(regular_result.stdout)
232+
233+
# Without continuous flag, Ls should be < 360
234+
self.assertLess(regular_values[0][1], 360)
235+
236+
# The difference should be approximately 360
237+
self.assertAlmostEqual(values[0][1], regular_values[0][1] + 360, delta=1)
238+
239+
def test_help_message(self):
240+
"""Test that help message can be displayed"""
241+
result = self.run_mars_calendar(['-h'])
242+
243+
# Check that something was printed
244+
self.assertTrue(len(result.stdout) > 0, "No help message generated")
245+
246+
# Check for typical help message components
247+
help_checks = [
248+
'usage:',
249+
'-ls',
250+
'-sol',
251+
'-my',
252+
'--continuous',
253+
'--debug'
254+
]
255+
256+
for check in help_checks:
257+
self.assertIn(check, result.stdout.lower(), f"Help message missing '{check}'")
258+
259+
260+
if __name__ == '__main__':
261+
unittest.main()

0 commit comments

Comments
 (0)