Skip to content

Commit 6a9b2c6

Browse files
authored
Improve reusable Integration Tests system (nasa#5187)
* Add InT for FileHandling subtopology: file uplink, file downlink * Add InT for CdhCore: command, events, telemetry ch * remove unused imports * add documentation for reusable InTs * Prune unused int_config.json entries from Ref * format python?? * fix incorrect usages of shared int infrastructure * fix comp instance name
1 parent e27016a commit 6a9b2c6

5 files changed

Lines changed: 234 additions & 41 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""test_cdh_core.py:
2+
3+
Test the core functionality of the CdhCore subtopology with:
4+
1. Commands
5+
2. Events
6+
3. Telemetry channels
7+
"""
8+
9+
import random
10+
from fprime_gds.common.testing_fw.api import IntegrationTestAPI
11+
12+
13+
def test_basic_command_and_event(fprime_test_api: IntegrationTestAPI):
14+
"""Test that we can send a command and receive the expected event in response"""
15+
16+
# Send NO_OP command to FSW and wait for expected event
17+
fprime_test_api.send_and_assert_command(
18+
f"{fprime_test_api.get_mnemonic('Svc.CommandDispatcher')}.CMD_NO_OP",
19+
)
20+
21+
22+
def test_command_and_event_with_string_arg(fprime_test_api: IntegrationTestAPI):
23+
"""Test that we can send a command with arguments and receive the expected event with args in response"""
24+
25+
TEST_STRING = f"test string {random.random()}"
26+
27+
test_event = fprime_test_api.get_event_pred("NoOpStringReceived", [TEST_STRING])
28+
29+
# Send NO_OP command to FSW and wait for expected event
30+
fprime_test_api.send_and_assert_event(
31+
f"{fprime_test_api.get_mnemonic('Svc.CommandDispatcher')}.CMD_NO_OP_STRING",
32+
[TEST_STRING],
33+
events=[test_event],
34+
timeout=2,
35+
)
36+
37+
38+
def test_command_and_event_with_many_args(fprime_test_api: IntegrationTestAPI):
39+
"""Test that we can send a command with arguments and receive the expected event with args in response"""
40+
41+
# types are (I32, F32, U8) - random float precision is finnicky, so just use a fixed value
42+
TEST_ARGS = [
43+
random.randint(-(2 ** 31), 2 ** 31 - 1),
44+
1.5,
45+
random.randint(0, 2 ** 8 - 1),
46+
]
47+
48+
test_event = fprime_test_api.get_event_pred("TestCmd1Args", TEST_ARGS)
49+
50+
# Send CMD_1 (no-op with args) command to FSW and wait for expected event
51+
fprime_test_api.send_and_assert_event(
52+
f"{fprime_test_api.get_mnemonic('Svc.CommandDispatcher')}.CMD_TEST_CMD_1",
53+
TEST_ARGS,
54+
events=[test_event],
55+
timeout=3,
56+
)
57+
58+
59+
def test_telemetry_update(fprime_test_api: IntegrationTestAPI):
60+
"""Test that we can receive telemetry updates with expected values"""
61+
62+
cmd_dispatched_channel = fprime_test_api.get_telemetry_pred("CommandsDispatched")
63+
64+
# Wait for telemetry update with expected values
65+
begin_result = fprime_test_api.await_telemetry(cmd_dispatched_channel, timeout=3)
66+
begin_tlm_val = begin_result.val_obj.val
67+
68+
# Send no op to increase the count of commands dispatched
69+
end_result = fprime_test_api.send_and_await_telemetry(
70+
f"{fprime_test_api.get_mnemonic('Svc.CommandDispatcher')}.CMD_NO_OP",
71+
channels=cmd_dispatched_channel,
72+
timeout=3,
73+
)
74+
# Assert that the telemetry value has increased by 1 after sending the command
75+
assert end_result.val_obj.val == begin_tlm_val + 1
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
"""test_file_handling.py:
2+
3+
Test the file handling functionality with:
4+
1. Uplink a file
5+
2. Downlink a file
6+
7+
Both these tests assume that FSW and GDS are running on the same system, for simplicity
8+
Should projects require to run on different systems, they are encouraged to adapt these tests as needed
9+
"""
10+
11+
import tempfile
12+
from pathlib import Path
13+
import random
14+
15+
16+
def test_uplink_file(fprime_test_api):
17+
"""IMPORTANT: These tests assume that FSW and GDS are running on the same system
18+
19+
Create a file locally, "uplink" it (FSW and GDS are expected to be the same system)
20+
and verify the contents are the same after the uplink process."""
21+
22+
TEST_DATA = f"test uplink data {random.random()}\n"
23+
24+
tmp_file_in = tempfile.NamedTemporaryFile(mode="w+")
25+
tmp_file_out = tempfile.NamedTemporaryFile(mode="r+")
26+
27+
tmp_file_in.write(TEST_DATA)
28+
tmp_file_in.flush()
29+
30+
# Begin file uplink and wait for completion event
31+
fprime_test_api.uplink_file_and_await_completion(
32+
tmp_file_in.name, destination=tmp_file_out.name, timeout=15
33+
)
34+
35+
tmp_file_out.seek(0)
36+
assert tmp_file_out.readlines() == [TEST_DATA]
37+
38+
39+
def test_downlink_file(fprime_test_api):
40+
"""IMPORTANT: These tests assume that FSW and GDS are running on the same system
41+
42+
Create a file locally, "downlink" it (FSW and GDS are expected to be the same system)
43+
and verify the contents are the same after the downlink process.
44+
"""
45+
TEST_DATA = f"test downlink data {random.random()}\n"
46+
47+
# Retrieve GDS sandbox area for file downlink
48+
down_store = Path(fprime_test_api.pipeline.down_store)
49+
if not down_store.exists():
50+
raise RuntimeError("GDS sandbox area not found for file downlink test")
51+
52+
# Add some randomness to avoid collisions in the downlink folder
53+
output_filename = f"downlink_test_{random.randint(0, 10000)}.txt"
54+
output_file = down_store / output_filename
55+
56+
# Using dir="/tmp/" to force short filenames which can be a limitation on downlink
57+
tmp_file_in = tempfile.NamedTemporaryFile(mode="w+", dir="/tmp/")
58+
tmp_file_in.write(TEST_DATA)
59+
tmp_file_in.flush()
60+
61+
# Request file downlink via FSW command
62+
fprime_test_api.send_and_assert_command(
63+
f"{fprime_test_api.get_mnemonic('Svc.FileDownlink')}.SendFile",
64+
[str(tmp_file_in.name), str(output_filename)],
65+
)
66+
67+
assert output_file.read_text() == TEST_DATA
Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,15 @@
11
{
2-
"Svc.ActiveRateGroup": "Ref.rateGroup1Comp",
32
"Svc.CommandDispatcher": "CdhCore.cmdDisp",
43
"Svc.CmdSequencer": "Ref.cmdSeq",
54
"Svc.FileDownlink": "FileHandling.fileDownlink",
65
"Svc.FileManager": "FileHandling.fileManager",
7-
"Svc.FileUplink": "Ref.fileUplink",
8-
"Ref.PingReceiver": "Ref.pingRcvr",
6+
"Svc.FileUplink": "FileHandling.fileUplink",
97
"Svc.EventManager": "CdhCore.events",
10-
"Svc.TlmChan": "Ref.tlmSend",
8+
"Svc.TlmChan": "CdhCore.tlmSend",
119
"Svc.PrmDb": "FileHandling.prmDb",
1210
"Svc.PrmDb.filename": "/tmp/PrmDb.dat",
1311
"Svc.DpCatalog": "DataProducts.dpCat",
1412
"Svc.DpManager": "DataProducts.dpMgr",
1513
"Svc.DpWriter": "DataProducts.dpWriter",
16-
"Svc.ComQueue": "Ref.comQueue",
17-
"Ref.TypeDemo": "Ref.typeDemo",
18-
"Svc.Health": "CdhCore.health",
19-
"Ref.SignalGen": "Ref.SG1",
20-
"Ref.SendBuff": "Ref.sendBuffComp",
21-
"Drv.TcpClient": "Ref.comDriver",
22-
"Svc.AssertFatalAdapter": "Ref.fatalAdapter",
23-
"Svc.FatalHandler": "Ref.fatalHandler",
24-
"Svc.BufferManager": "Ref.commsBufferManager",
25-
"Svc.PosixTime": "Ref.posixTime",
26-
"Svc.RateGroupDriver": "Ref.rateGroupDriverComp",
27-
"Ref.RecvBuff": "Ref.recvBuffComp",
28-
"Svc.Version": "CdhCore.version",
29-
"Svc.PassiveTextLogger": "Ref.textLogger",
30-
"Svc.SystemResources": "Ref.systemResources",
31-
"Svc.BufferManager": "Ref.dpBufferManager",
32-
"Svc.FrameAccumulator": "Ref.frameAccumulator",
33-
"Svc.FprimeDeframer": "Ref.deframer",
34-
"Svc.FprimeRouter": "Ref.fprimeRouter",
35-
"Svc.FprimeFramer": "Ref.framer",
36-
"Svc.ComStub": "Ref.comStub"
14+
"Svc.Health": "CdhCore.health"
3715
}

TestDeploymentsProject/Ref/test/int/test_cmd_parameter.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,93 +25,93 @@ def test_send_parameter(fprime_test_api):
2525

2626
## setup default-value
2727
fprime_test_api.send_and_assert_command(
28-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER1_PRM_SET",
28+
"Ref.recvBuffComp.PARAMETER1_PRM_SET",
2929
[1],
3030
max_delay=5,
3131
)
3232
fprime_test_api.send_and_assert_command(
33-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER2_PRM_SET",
33+
"Ref.recvBuffComp.PARAMETER2_PRM_SET",
3434
[2],
3535
max_delay=5,
3636
)
3737
fprime_test_api.send_and_assert_command(
38-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER3_PRM_SET",
38+
"Ref.sendBuffComp.PARAMETER3_PRM_SET",
3939
[3],
4040
max_delay=5,
4141
)
4242
fprime_test_api.send_and_assert_command(
43-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER4_PRM_SET",
43+
"Ref.sendBuffComp.PARAMETER4_PRM_SET",
4444
[4],
4545
max_delay=5,
4646
)
4747

4848
# Only work if send command PARAMETER1_PRM_SET then check telemetry. Unsigned integer 0..4294967295)
4949
fprime_test_api.send_and_assert_command(
50-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER1_PRM_SET",
50+
"Ref.recvBuffComp.PARAMETER1_PRM_SET",
5151
[10],
5252
max_delay=5,
5353
)
5454

5555
# Check Telem only will not work
5656
param1_change = fprime_test_api.get_telemetry_pred(
57-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "Parameter1", 10
57+
"Ref.recvBuffComp.Parameter1", 10
5858
)
5959
fprime_test_api.assert_telemetry(param1_change, timeout=5)
6060

6161
# Send PARAMETER1_PRM_SAVE
6262
fprime_test_api.send_and_assert_command(
63-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER1_PRM_SAVE",
63+
"Ref.recvBuffComp.PARAMETER1_PRM_SAVE",
6464
max_delay=1,
6565
)
6666

6767
# Send PARAMETER2_PRM_SET (confirm new value / SAVE ) signed integer -32867 and 32767
6868
fprime_test_api.send_and_assert_command(
69-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER2_PRM_SET",
69+
"Ref.recvBuffComp.PARAMETER2_PRM_SET",
7070
[20],
7171
max_delay=5,
7272
)
7373

7474
param2_change = fprime_test_api.get_telemetry_pred(
75-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "Parameter2", 20
75+
"Ref.recvBuffComp.Parameter2", 20
7676
)
7777
fprime_test_api.assert_telemetry(param2_change, timeout=5)
7878

7979
fprime_test_api.send_and_assert_command(
80-
fprime_test_api.get_mnemonic("Ref.RecvBuff") + "." + "PARAMETER2_PRM_SAVE",
80+
"Ref.recvBuffComp.PARAMETER2_PRM_SAVE",
8181
max_delay=5,
8282
)
8383

8484
# Send PARAMETER3_PRM_SET (confirm new value / SAVE ) unsigned integer 0..255
8585
fprime_test_api.send_and_assert_command(
86-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER3_PRM_SET",
86+
"Ref.sendBuffComp.PARAMETER3_PRM_SET",
8787
[30],
8888
max_delay=5,
8989
)
9090

9191
param3_change = fprime_test_api.get_telemetry_pred(
92-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "Parameter3", 30
92+
"Ref.sendBuffComp.Parameter3", 30
9393
)
9494
fprime_test_api.assert_telemetry(param3_change, timeout=5)
9595

9696
fprime_test_api.send_and_assert_command(
97-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER3_PRM_SAVE",
97+
"Ref.sendBuffComp.PARAMETER3_PRM_SAVE",
9898
max_delay=5,
9999
)
100100

101101
# Send PARAMETER4_PRM_SET (confirm new value / SAVE ) float
102102
fprime_test_api.send_and_assert_command(
103-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER4_PRM_SET",
103+
"Ref.sendBuffComp.PARAMETER4_PRM_SET",
104104
[40],
105105
max_delay=5,
106106
)
107107

108108
param4_change = fprime_test_api.get_telemetry_pred(
109-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "Parameter4", 40
109+
"Ref.sendBuffComp.Parameter4", 40
110110
)
111111
fprime_test_api.assert_telemetry(param4_change, timeout=5)
112112

113113
fprime_test_api.send_and_assert_command(
114-
fprime_test_api.get_mnemonic("Ref.SendBuff") + "." + "PARAMETER4_PRM_SAVE",
114+
"Ref.sendBuffComp.PARAMETER4_PRM_SAVE",
115115
max_delay=5,
116116
)
117117

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Reusable Integration Tests
2+
3+
F´ ships portable integration tests that exercise framework components (`Svc/*`) and standard subtopologies (`Svc/Subtopologies/*`) against any deployment. They are written once in F´ and reused unchanged in your project — you only supply a small JSON file mapping framework component names to your deployment's instance names.
4+
5+
This guide shows how to run them in your own deployment.
6+
7+
## How it works
8+
9+
Reusable tests live next to the component or subtopology under `test/int/` (e.g. [Svc/CmdDispatcher/test/int/test_cmd_dispatcher.py](../../../Svc/CmdDispatcher/test/int/test_cmd_dispatcher.py), [Svc/Subtopologies/CdhCore/test/int/test_cdh_core.py](../../../Svc/Subtopologies/CdhCore/test/int/test_cdh_core.py)). Instead of hardcoding instance mnemonics like `Ref.cmdDisp`, they call `fprime_test_api.get_mnemonic(...)` with the qualified component name:
10+
11+
```python
12+
fprime_test_api.send_and_assert_command(
13+
f"{fprime_test_api.get_mnemonic('Svc.CommandDispatcher')}.CMD_NO_OP",
14+
)
15+
```
16+
17+
At runtime, `get_mnemonic("Svc.CommandDispatcher")` reads a JSON file (`int_config.json` by convention) supplied via the `--deployment-config` pytest flag and returns the deployment-specific instance mnemonic — e.g. `CdhCore.cmdDisp` for the Ref deployment. If a key is missing, it falls back to the qualified name itself, so a partial config still works.
18+
19+
Source: `IntegrationTestAPI.get_mnemonic` in [fprime-gds api.py](https://github.com/nasa/fprime-gds/blob/devel/src/fprime_gds/common/testing_fw/api.py).
20+
21+
## Writing your `int_config.json`
22+
23+
Create one JSON file in your deployment (typical location: `<MyDeployment>/test/int/int_config.json`) mapping each `Svc.*` (and any other reusable) component name to the instance mnemonic in your topology:
24+
25+
```json
26+
{
27+
"Svc.CommandDispatcher": "CdhCore.cmdDisp",
28+
"Svc.CmdSequencer": "Ref.cmdSeq",
29+
"Svc.FileDownlink": "FileHandling.fileDownlink",
30+
"Svc.FileManager": "FileHandling.fileManager",
31+
... add more as needed per the tests you want to run ...
32+
}
33+
```
34+
35+
The full reference example is [TestDeploymentsProject/Ref/test/int/int_config.json](../../../TestDeploymentsProject/Ref/test/int/int_config.json) — copy it and replace the right-hand side with your instance names. Only include the components whose tests you intend to run.
36+
37+
## Running the tests
38+
39+
Build and start your deployment, then start the GDS pointed at your dictionary, and run pytest against the `test/int` directory of any component or subtopology you want to exercise:
40+
41+
```bash
42+
# 1. Start GDS + flight software (separate terminal, from your deployment dir)
43+
fprime-gds --dictionary build-artifacts/*/MyDeployment/dict/MyDeploymentTopologyDictionary.json
44+
45+
# 2. Run the reusable tests
46+
pytest ./lib/fprime/Svc/CmdDispatcher/test/int \
47+
./lib/fprime/Svc/Subtopologies/CdhCore/test/int \
48+
--dictionary build-artifacts/*/MyDeployment/dict/MyDeploymentTopologyDictionary.json \
49+
--deployment-config ./MyDeployment/test/int/int_config.json
50+
```
51+
52+
`--deployment-config` is provided by the `fprime-gds` pytest plugin (see [pytest_integration.py](https://github.com/nasa/fprime-gds/blob/devel/src/fprime_gds/common/testing_fw/pytest_integration.py)). All other flags (`--dictionary`, `--logs`, etc.) are the standard GDS pipeline options.
53+
54+
## CI example
55+
56+
The Ref deployment's CI job is the canonical reference — see [`.github/workflows/ref.yml`](../../../.github/workflows/ref.yml):
57+
58+
```yaml
59+
- name: "Integration Tests"
60+
uses: nasa/fprime-actions/run-integration-tests@devel
61+
with:
62+
test-working-directory: "TestDeploymentsProject/Ref/test/int"
63+
binary: "build-artifacts/*/Ref/bin/Ref"
64+
gds-args: "--dictionary build-artifacts/*/Ref/dict/RefTopologyDictionary.json"
65+
pytest-args: "--deployment-config ${{ github.workspace }}/TestDeploymentsProject/Ref/test/int/int_config.json"
66+
```
67+
68+
To also run the framework's reusable tests, pass extra paths via `pytest-args` to include the relevant `Svc/*/test/int/test_*.py` test files.
69+
70+
## See also
71+
72+
- [GDS Integration Test API Guide](gds-test-api-guide.md) — full `IntegrationTestAPI` reference
73+
- [Ref `int_config.json`](../../../TestDeploymentsProject/Ref/test/int/int_config.json) — complete example

0 commit comments

Comments
 (0)