Resolve devloop audit findings
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
4b880d28887b366bdf0656b25e67b4a607d0b91a- Parents
-
209b581 - Tree
4ac1c6d
4b880d2
4b880d28887b366bdf0656b25e67b4a607d0b91a209b581
4ac1c6d| Status | File | + | - |
|---|---|---|---|
| M |
README.md
|
2 | 1 |
| M |
src/fgof_devloop.f90
|
5 | 1 |
| M |
test/test_devloop_jobs.f90
|
7 | 0 |
| M |
test/test_devloop_model.f90
|
15 | 1 |
README.mdmodified@@ -95,13 +95,14 @@ Current semantics: | ||
| 95 | 95 | - `devloop_watch_summary` condenses `fgof-watch` event batches into file, directory, create, modify, remove, move, ignored, and failure counters |
| 96 | 96 | - `devloop_watch_options()` projects dev-loop policy into `fgof-watch` options for debounce polls, hidden-path filtering, and directory event emission |
| 97 | 97 | - `devloop_watch_trigger()` turns successful watch summaries into change triggers while suppressing watcher failures, disabled restart-on-change policy, empty batches, directory-only batches when disabled, and batches below `min_restart_changes` |
| 98 | +- `devloop_change_trigger()` suppresses nonpositive change counts so empty batches cannot start cycles through the lower-level trigger API | |
| 98 | 99 | - `devloop_build_command()`, `devloop_run_command()`, and `devloop_smoke_command()` wrap `fgof-process` commands with loop roles and optional process options |
| 99 | 100 | - `run_devloop_command()` executes one command spec and preserves the raw `process_result`, including stdout, stderr, exit code, timeout state, and process error details |
| 100 | 101 | - `run_devloop_cycle()` starts a cycle, executes enabled build/run/smoke specs in order, skips later specs after the first failure, and feeds the outcome into `finish_devloop_cycle()` |
| 101 | 102 | - `devloop_service_job()` builds a long-running service/job spec backed by `fgof-jobs` |
| 102 | 103 | - `attach_devloop_job()` records an already-launched pid/process group and ownership expectations |
| 103 | 104 | - `observe_devloop_job()` applies `fgof-jobs` wait results while preserving member-level terminal state |
| 104 | -- `devloop_job_restart_plan()` models whether a watched change should stop, start, restart, release, or require terminal handoff for a long-running job; released jobs return no action | |
| 105 | +- `devloop_job_restart_plan()` models whether a watched change should stop, start, restart, release, or require terminal handoff for a long-running job; released jobs and release-on-handoff plans return no stop/start/restart action | |
| 105 | 106 | - `begin_devloop_cycle()` increments the cycle counter and starts work only when the loop is active, idle, and policy permits the trigger |
| 106 | 107 | - `finish_devloop_cycle()` records success or failure and returns an explicit decision to idle, restart, or stop |
| 107 | 108 | - negative `max_failures` values normalize to unlimited failures |
src/fgof_devloop.f90modified@@ -354,8 +354,10 @@ contains | ||
| 354 | 354 | type(devloop_trigger) :: trigger |
| 355 | 355 | |
| 356 | 356 | trigger = clear_devloop_trigger() |
| 357 | + if (change_count <= 0) return | |
| 358 | + | |
| 357 | 359 | trigger%kind = FGOF_DEVLOOP_TRIGGER_CHANGE |
| 358 | - trigger%change_count = max(0, change_count) | |
| 360 | + trigger%change_count = change_count | |
| 359 | 361 | if (present(reason)) then |
| 360 | 362 | trigger%reason = reason |
| 361 | 363 | else |
@@ -693,6 +695,8 @@ contains | ||
| 693 | 695 | |
| 694 | 696 | if (job_state%terminal_handoff_required .and. job_state%spec%release_on_handoff) then |
| 695 | 697 | plan%should_release = .true. |
| 698 | + plan%reason = "release for terminal handoff" | |
| 699 | + return | |
| 696 | 700 | end if |
| 697 | 701 | |
| 698 | 702 | if (job_state%running .or. job_state%stopped .or. job_state%cleanup_needed) then |
test/test_devloop_jobs.f90modified@@ -83,6 +83,13 @@ contains | ||
| 83 | 83 | |
| 84 | 84 | if (.not. plan%terminal_handoff_required) error stop "foreground jobs should surface terminal handoff" |
| 85 | 85 | if (.not. plan%should_release) error stop "release-on-handoff should be explicit" |
| 86 | + if (plan%action /= FGOF_DEVLOOP_JOB_ACTION_NONE) error stop "release-on-handoff should not restart" | |
| 87 | + if (plan%should_stop) error stop "release-on-handoff should not stop the job" | |
| 88 | + if (plan%should_start) error stop "release-on-handoff should not start a replacement" | |
| 89 | + if (plan%should_restart) error stop "release-on-handoff should not request restart" | |
| 90 | + if (plan%reason /= "release for terminal handoff") then | |
| 91 | + error stop "release-on-handoff reason should be explicit" | |
| 92 | + end if | |
| 86 | 93 | end subroutine test_terminal_handoff_plan |
| 87 | 94 | |
| 88 | 95 | subroutine test_pipeline_wait_observation() |
test/test_devloop_model.f90modified@@ -4,6 +4,7 @@ program test_devloop_model | ||
| 4 | 4 | FGOF_DEVLOOP_DECISION_RESTART, & |
| 5 | 5 | FGOF_DEVLOOP_DECISION_STOP, & |
| 6 | 6 | FGOF_DEVLOOP_TRIGGER_CHANGE, & |
| 7 | + FGOF_DEVLOOP_TRIGGER_NONE, & | |
| 7 | 8 | FGOF_DEVLOOP_TRIGGER_START, & |
| 8 | 9 | begin_devloop_cycle, & |
| 9 | 10 | clear_devloop_options, & |
@@ -15,13 +16,14 @@ program test_devloop_model | ||
| 15 | 16 | should_start_on_open, & |
| 16 | 17 | start_devloop, & |
| 17 | 18 | stop_devloop |
| 18 | - use fgof_devloop_types, only : devloop_cycle, devloop_decision, devloop_options, devloop_state | |
| 19 | + use fgof_devloop_types, only : devloop_cycle, devloop_decision, devloop_options, devloop_state, devloop_trigger | |
| 19 | 20 | implicit none |
| 20 | 21 | |
| 21 | 22 | type(devloop_state) :: state |
| 22 | 23 | type(devloop_options) :: options |
| 23 | 24 | type(devloop_cycle) :: cycle |
| 24 | 25 | type(devloop_decision) :: decision |
| 26 | + type(devloop_trigger) :: trigger | |
| 25 | 27 | |
| 26 | 28 | state = clear_devloop_state() |
| 27 | 29 | call start_devloop(state) |
@@ -48,6 +50,18 @@ program test_devloop_model | ||
| 48 | 50 | if (.not. decision%should_run) error stop "restart decision should request another run" |
| 49 | 51 | if (state%consecutive_failures /= 1) error stop "failed cycle should increment failure count" |
| 50 | 52 | |
| 53 | + call start_devloop(state) | |
| 54 | + trigger = devloop_change_trigger(0) | |
| 55 | + if (trigger%kind /= FGOF_DEVLOOP_TRIGGER_NONE) then | |
| 56 | + error stop "zero-count change trigger should be suppressed" | |
| 57 | + end if | |
| 58 | + trigger = devloop_change_trigger(-2) | |
| 59 | + if (trigger%kind /= FGOF_DEVLOOP_TRIGGER_NONE) then | |
| 60 | + error stop "negative-count change trigger should be suppressed" | |
| 61 | + end if | |
| 62 | + cycle = begin_devloop_cycle(state, devloop_change_trigger(0)) | |
| 63 | + if (cycle%started) error stop "zero-count changes should not start a cycle" | |
| 64 | + | |
| 51 | 65 | options = clear_devloop_options() |
| 52 | 66 | options%restart_on_change = .false. |
| 53 | 67 | call start_devloop(state, options) |