|
root / utils / replace_probe / src / main.reef
main.reef Reef 216 lines 7.7 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/******************************************************************************
                __               ____                __
               / /   ___  ____ _/ __/_____________ _/ /__
              / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
             / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
            /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/

    (C)opyright 2025-2026, Leafscale, LLC -  https://www.leafscale.com

    Project: zyginit
   Filename: main.reef
    Authors: Chris Tusa <chris.tusa@leafscale.com>
    License: <see LICENSE file included with this source code>
Description: Proberound-trip the state.toml format through replace.reef

******************************************************************************/

import supervisor
import replace
import io.dir as dir
import io.file
import core.str
import sys.env as sysenv
import sys.process as process

mut g_pass: int = 0
mut g_fail: int = 0

proc check(label: string, ok: bool)
    if ok
        g_pass = g_pass + 1
        println("  PASS: " + label)
    else
        g_fail = g_fail + 1
        println("  FAIL: " + label)
    end if
end check

fn build_synthetic_table(): supervisor.ServiceTable
    let table = supervisor.new_service_table(8)

    // sshd: RUNNING with restart_count=2 and last_exit_code=0
    let def_sshd = supervisor.make_def_with_name("sshd")
    let idx_sshd = supervisor.add_service(table, def_sshd)
    supervisor.adopt_runtime(table, idx_sshd, supervisor.STATE_RUNNING(),
        100, 23, 2, 0, 1746204051)

    // syslogd: RUNNING with no restarts, last_exit_code = -1 (never exited)
    let def_syslog = supervisor.make_def_with_name("syslogd")
    let idx_syslog = supervisor.add_service(table, def_syslog)
    supervisor.adopt_runtime(table, idx_syslog, supervisor.STATE_RUNNING(),
        101, 17, 0, 0 - 1, 1746204047)

    // cron: STOPPED — must NOT be serialized
    let def_cron = supervisor.make_def_with_name("cron")
    let idx_cron = supervisor.add_service(table, def_cron)
    supervisor.adopt_runtime(table, idx_cron, supervisor.STATE_STOPPED(),
        0 - 1, 0 - 1, 0, 0, 0)

    return table
end build_synthetic_table

proc test_round_trip()
    println("== round-trip ==")
    let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
    let probe_dir = str.concat(tmpdir, "/replace_probe")
    let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)

    if not dir.dir_exists(probe_dir)
        let _ = dir.create_dir_all(probe_dir)
    end if

    let table = build_synthetic_table()
    let serialized = replace.serialize_state(table, 1746204000)
    check("serialize returns true", serialized)

    let recovered = replace.recover_state(1746204000)
    check("recover boot_time matches", replace.rs_boot_time(recovered) == 1746204000)
    check("recover count == 2 (running only)", replace.rs_count(recovered) == 2)

    // Find services by name (order is implementation-defined)
    mut found_sshd = false
    mut found_syslog = false
    mut i = 0
    while i < replace.rs_count(recovered)
        let svc = replace.rs_service(recovered, i)
        let name = replace.svc_name(svc)
        if name == "sshd"
            found_sshd = true
            check("sshd contract_id = 23", replace.svc_contract_id(svc) == 23)
            check("sshd restart_count = 2", replace.svc_restart_count(svc) == 2)
            check("sshd last_exit_code = 0", replace.svc_last_exit_code(svc) == 0)
            check("sshd last_start_time = 1746204051", replace.svc_last_start_time(svc) == 1746204051)
        elif name == "syslogd"
            found_syslog = true
            check("syslogd contract_id = 17", replace.svc_contract_id(svc) == 17)
            check("syslogd last_exit_code = -1", replace.svc_last_exit_code(svc) == 0 - 1)
        end if
        i = i + 1
    end while
    check("found sshd", found_sshd)
    check("found syslogd", found_syslog)

    // After recover_state, state.toml is deleted
    check("state.toml deleted after recovery", not file.fileExists(replace.STATE_FILE_PATH()))
end test_round_trip

proc test_stale_boot_time()
    println("== stale state file ==")
    let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
    let probe_dir = str.concat(tmpdir, "/replace_probe")
    let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)

    if not dir.dir_exists(probe_dir)
        let _ = dir.create_dir_all(probe_dir)
    end if

    let table = build_synthetic_table()
    let _ = replace.serialize_state(table, 1746204000)

    // Recover with DIFFERENT boot_time -> stale -> empty + deleted
    let recovered = replace.recover_state(1746999999)
    check("stale file -> empty state", replace.rs_is_empty(recovered))
    check("stale file deleted", not file.fileExists(replace.STATE_FILE_PATH()))
end test_stale_boot_time

proc test_missing_file()
    println("== missing state file ==")
    let tmpdir = sysenv.get_env_or("TMPDIR", "/tmp")
    // Unique per-run dir to guarantee no leftover state.toml
    let probe_dir = str.concat(tmpdir, "/replace_probe_missing")
    let _ = sysenv.set_env("ZYGINIT_RUN_DIR", probe_dir)

    if not dir.dir_exists(probe_dir)
        let _ = dir.create_dir_all(probe_dir)
    end if

    // No need to unlink — the dir is fresh per invocation; if a prior
    // run left a state.toml here, the boot_time will differ and recovery
    // will treat it as stale (exercising a different code path). Either
    // way the test asserts "empty state" which holds.

    let recovered = replace.recover_state(1746204000)
    check("missing-or-stale file -> empty state", replace.rs_is_empty(recovered))
end test_missing_file

proc test_preconditions()
    println("== preconditions ==")
    let table = supervisor.new_service_table(8)

    // sshd RUNNING — stable
    let def_sshd = supervisor.make_def_with_name("sshd")
    let idx_sshd = supervisor.add_service(table, def_sshd)
    supervisor.adopt_runtime(table, idx_sshd, supervisor.STATE_RUNNING(),
        100, 23, 0, 0 - 1, 1746204051)

    let r1 = replace.check_preconditions(table)
    check("all-RUNNING -> empty report", str.length(r1) == 0)

    // Add a STOPPING service
    let def_cron = supervisor.make_def_with_name("cron")
    let idx_cron = supervisor.add_service(table, def_cron)
    supervisor.adopt_runtime(table, idx_cron, supervisor.STATE_STOPPING(),
        101, 24, 0, 0 - 1, 1746204060)

    let r2 = replace.check_preconditions(table)
    check("STOPPING -> non-empty report", str.length(r2) > 0)
    check("report mentions cron", str.index_of(r2, "cron") >= 0)
    check("report mentions stopping", str.index_of(r2, "stopping") >= 0)

    // Add a WAITING service — should also be flagged transient
    let def_syslogd = supervisor.make_def_with_name("syslogd")
    let idx_syslogd = supervisor.add_service(table, def_syslogd)
    supervisor.adopt_runtime(table, idx_syslogd, supervisor.STATE_WAITING(),
        0 - 1, 0 - 1, 1, 1, 0)

    let r3 = replace.check_preconditions(table)
    check("WAITING also flagged", str.index_of(r3, "syslogd") >= 0)
    check("multiple entries comma-separated", str.index_of(r3, ", ") >= 0)
end test_preconditions

proc main()
    test_round_trip()
    test_stale_boot_time()
    test_missing_file()
    test_preconditions()

    println("")
    println("--- " + int_to_str(g_pass) + " passed, " + int_to_str(g_fail) + " failed ---")
    if g_fail > 0
        process.exit_now(1)
    end if
end main

fn int_to_str(n: int): string
    if n == 0
        return "0"
    end if
    mut value = n
    mut neg = false
    if n < 0
        value = 0 - n
        neg = true
    end if
    mut result = ""
    while value > 0
        let digit = value % 10
        result = str.concat(str.substring("0123456789", digit, 1), result)
        value = value / 10
    end while
    if neg
        result = str.concat("-", result)
    end if
    return result
end int_to_str