Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions .github/workflows/record-missions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Record missions

# Re-records the replay tests' fixtures (src/test/missions/fixtures) under
# pHelmIvP. Download the "fixtures" artifact and commit it.
on:
workflow_dispatch:
inputs:
missions:
description: "Missions to record, separated by spaces (default: all of missions.txt)"
required: false
default: ""

defaults:
run:
shell: bash

env:
DEBIAN_FRONTEND: noninteractive

jobs:
record:
runs-on: ubuntu-24.04
container: ubuntu:24.04
steps:
- name: Install git
run: apt-get update && apt-get install -y --no-install-recommends git ca-certificates

- uses: actions/checkout@v5

- name: Init
run: |
git config --global --add safe.directory '*'
./init.sh
# all of MOOS-IvP, not just the helm libraries
apt-get install -y --no-install-recommends make patch

- name: Record
env:
MISSIONS: ${{ inputs.missions }}
run: src/test/missions/record.sh ${MISSIONS}

- uses: actions/upload-artifact@v4
with:
name: fixtures
path: src/test/missions/fixtures
3 changes: 2 additions & 1 deletion init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ apt_install() {
}

# moos-ivp's helm libraries (IVP_BUILD_HELM_LIBS_ONLY) need only CMake and a compiler
packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobuf-dev curl gpg)
# xz-utils: the replay tests' recorded missions are compressed
packages=(cmake ninja-build g++ git ca-certificates protobuf-compiler libprotobuf-dev curl gpg xz-utils)
$SUDO apt-get update
apt_install "${packages[@]}"

Expand Down
81 changes: 54 additions & 27 deletions src/lib/helm/helm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ struct Value
bool is_string{false};
double d{0};
std::string s;
// posted by the helm's "initialize"
bool init{false};
};

Value make_value(const VarDataPair& pair)
Expand Down Expand Up @@ -139,6 +141,7 @@ class Helm::Impl
void start(double time);

// mail
void apply_pending(double time);
void apply(const Navigation& nav, double time);
void apply(const Command& command, double time);
void apply_variable(const std::string& var, const Value& value, double msg_time);
Expand Down Expand Up @@ -341,37 +344,30 @@ const Output& Helm::Impl::iterate(double time)
output_ = Output();
curr_time_ = time;

// the previous iteration's posts reach the helm first, as they reached the MOOSDB first
info_buffer_.setCurrTime(time);
ledger_.setCurrTimeUTC(time);
if (!started_)
{
// pHelmIvP posts its "initialize" variables before it registers for
// anything, so they replace whatever was posted before it started
if (config_warnings_.empty())
apply_pending(time);
start(time);
}

// The previous iteration's posts (or the startup's) reach the helm first,
// as they reached the MOOSDB first
auto echo = std::move(posted_);
posted_.clear();
const double echo_time = posted_time_ == 0 ? time : posted_time_;
posted_time_ = time;

if (!started_)
start(time);

info_buffer_.setCurrTime(time);
ledger_.setCurrTimeUTC(time);
if (status_ != Status::MALCONFIG)
{
for (const auto& [var, value] : echo)
if (registered_.count(var))
apply_variable(var, value, echo_time);
for (const auto& input : pending_)
{
if (const auto* nav = std::get_if<Navigation>(&input))
apply(*nav, time);
else if (const auto* var = std::get_if<Variable>(&input))
{
Value value;
value.is_string = var->value_case() == Variable::kSval;
value.d = var->dval();
value.s = var->sval();
apply_variable(var->key(), value, seconds(var->time(), time));
}
else
apply(std::get<Command>(input), time);
}
apply_pending(time);
info_buffer_.setValue("COMMS_POLICY", comms_policy_, time);
}
pending_.clear();
Expand Down Expand Up @@ -434,6 +430,26 @@ const Output& Helm::Impl::iterate(double time)
return output_;
}

void Helm::Impl::apply_pending(double time)
{
for (const auto& input : pending_)
{
if (const auto* nav = std::get_if<Navigation>(&input))
apply(*nav, time);
else if (const auto* var = std::get_if<Variable>(&input))
{
Value value;
value.is_string = var->value_case() == Variable::kSval;
value.d = var->dval();
value.s = var->sval();
apply_variable(var->key(), value, seconds(var->time(), time));
}
else
apply(std::get<Command>(input), time);
}
pending_.clear();
}

void Helm::Impl::apply(const Navigation& nav, double time)
{
const auto& n = nav.nav();
Expand Down Expand Up @@ -519,14 +535,26 @@ void Helm::Impl::apply(const Command& command, double time)
// HelmIvP::OnNewMail for one message
void Helm::Impl::apply_variable(const std::string& var, const Value& value, double msg_time)
{
// pHelmIvP has set the helm's own "initialize" values already, but still
// acts on them (e.g. initialize MOOS_MANUAL_OVERRIDE = false)
auto store = [&]()
{
if (value.init)
return;
if (value.is_string)
info_buffer_.setValue(var, value.s, msg_time);
else
info_buffer_.setValue(var, value.d, msg_time);
};

const std::string sval = value.s;
if (var == "MOOS_MANUAL_OVERRIDE" || var == "MOOS_MANUAL_OVERIDE")
{
if (toupper(sval) == "FALSE")
set_manual_override(false);
else if (toupper(sval) == "TRUE")
set_manual_override(true);
info_buffer_.setValue(var, sval, msg_time);
store();
}
// Status is always complete, so there's nothing to rejournal
else if (var == "IVPHELM_STATE" || var == "DB_CLIENTS" || var == "RESTART_HELM" ||
Expand All @@ -548,14 +576,12 @@ void Helm::Impl::apply_variable(const std::string& var, const Value& value, doub
info_buffer_.setValue(toupper(vname) + "_NAV_GROUP", ledger_.getGroup(vname));
info_buffer_.setValue(toupper(vname) + "_NAV_TYPE", ledger_.getType(vname));
}
info_buffer_.setValue(var, sval, msg_time);
store();
}
else if (var == "HELM_MAP_CLEAR")
refresh_pending_ = true;
else if (value.is_string)
info_buffer_.setValue(var, sval, msg_time);
else
info_buffer_.setValue(var, value.d, msg_time);
store();
}

void Helm::Impl::set_manual_override(bool on)
Expand Down Expand Up @@ -668,8 +694,9 @@ void Helm::Impl::post_initial_variables(bool deferred)
info_buffer_.setValue(var, value.s);
else
info_buffer_.setValue(var, value.d);
// the MOOSDB echoes these too, but pHelmIvP ignores that echo (HELM_VAR_INIT)
add_posting(Posting::INITIALIZE, "", var, value);
value.init = true;
notify(var, value);
}
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,13 @@ add_test(NAME helm_libs COMMAND check_helm_libs)
add_executable(test_helm helm/test_helm.cpp)
target_link_libraries(test_helm PRIVATE ivpico_helm)
add_test(NAME helm COMMAND test_helm)

add_executable(replay missions/replay.cpp)
target_link_libraries(replay PRIVATE ivpico_helm)
file(GLOB fixtures LIST_DIRECTORIES true ${CMAKE_CURRENT_SOURCE_DIR}/missions/fixtures/*)
foreach(fixture ${fixtures})
if(IS_DIRECTORY ${fixture})
get_filename_component(mission ${fixture} NAME)
add_test(NAME replay_${mission} COMMAND replay ${fixture})
endif()
endforeach()
19 changes: 19 additions & 0 deletions src/test/missions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Mission replay tests

*This section was written by Claude.*

These tests check that the IvPico helm (`src/lib/helm`) does what pHelmIvP does, using MOOS-IvP's own missions.

```mermaid
flowchart LR
record["record.sh<br/>(all of MOOS-IvP, headless)"] -- "alog, .moos, .bhv" --> fixtures["fixtures/&lt;mission&gt;"]
fixtures --> replay["replay<br/>(ctest: replay_&lt;mission&gt;)"]
replay -- "same inputs, same iteration times" --> helm["ivpico::helm::Helm"]
replay -- "compare" --> report["pass / fail per stream"]
```

- `missions.txt` lists the missions and the variables poked to start them.
- `record.sh [mission...]` builds all of MOOS-IvP from `external/moos-ivp` into `build/missions`, runs each mission under pHelmIvP with time warp, and keeps each vehicle's helm inputs and outputs in `fixtures/<mission>`. The "Record missions" workflow runs it on GitHub and uploads the fixtures as an artifact.
- `replay <fixture>` feeds the recorded inputs to the IvPico helm at pHelmIvP's iteration times and compares what each helm posted. `replay.cpp` describes what's compared and the tolerances.

Re-record the fixtures whenever `external/moos-ivp` moves.
91 changes: 91 additions & 0 deletions src/test/missions/fixtures/s10_juliet/juliet.bhv
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//-------- FILE: juliet.bhv -------------

initialize DEPLOY = false
initialize RETURN = false
initialize GOTO_DEPTH = false

//========================================================
// Hierarchical Mode Declarations
// o ROOT o ROOT
// |--o INACTIVE |--o GOTO_DEPTH
// |--o ACTIVE |--o NOT GOTO_TDEPTH
// |--o RETURNING
// |--o LOITERING
//========================================================


Set MODE = ACTIVE {
DEPLOY = true
} INACTIVE

set MODE = RETURNING {
MODE = ACTIVE
RETURN = true
} LOITERING

//----------------------------------------------
Behavior = BHV_Loiter
{
name = loiter
pwt = 100
condition = MODE == LOITERING
updates = UP_LOITER

speed = 1.3
clockwise = best
radius = 4.0
nm_radius = 15.0
polygon = radial:: x=20, y=-80, radius=20, pts=8, snap=1, label=juliet_loiter
visual_hints = nextpt_color=red, nextpt_lcolor=khaki, edge_color=khaki
visual_hints = edge_size=1, vertex_size=4, vertex_color=white
}

//----------------------------------------------
Behavior = BHV_Waypoint
{
name = waypt_return
pwt = 100
condition = MODE == RETURNING
updates = UPDATES_RETURN
perpetual = true
endflag = RETURNING = false
endflag = DEPLOY = false

speed = 2.0
radius = 2.0
nm_radius = 8.0
point = 0,0
}

//----------------------------------------------
Behavior = BHV_GoToDepth
{
name = bhv_go_to_depth
pwt = 1000
condition = MODE == LOITERING
condition = GOTO_DEPTH = true
endflag = GOTO_DEPTH = false

depth = 40,60:30,45:20,45
repeat = 2
perpetual = true
capture_flag = DEPTH_ACHIEVED
capture_delta = 0.5
}

//----------------------------------------------
Behavior = BHV_ConstantDepth
{
name = bhv_const_depth
pwt = 100
duration = no-time-limit
condition = MODE==LOITERING
condition = GOTO_DEPTH != true
updates = DEPTH_VALUE

depth = 50
peakwidth = 8
basewidth = 12
summitdelta = 10
}

Binary file not shown.
Loading