This commit is contained in:
bQUARKz 2026-02-20 15:21:20 +00:00
parent bd507aeaa5
commit 8d1e0a57d6
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 232 additions and 73 deletions

10
Cargo.lock generated
View File

@ -1653,6 +1653,16 @@ dependencies = [
"winit", "winit",
] ]
[[package]]
name = "prometeu-layer-tests"
version = "0.1.0"
dependencies = [
"prometeu-bytecode",
"prometeu-hal",
"prometeu-test-support",
"prometeu-vm",
]
[[package]] [[package]]
name = "prometeu-system" name = "prometeu-system"
version = "0.1.0" version = "0.1.0"

View File

@ -12,6 +12,7 @@ members = [
"crates/tools/prometeu-cli", "crates/tools/prometeu-cli",
"crates/dev/prometeu-test-support", "crates/dev/prometeu-test-support",
"crates/dev/prometeu-layer-tests",
] ]
resolver = "2" resolver = "2"

View File

@ -0,0 +1,11 @@
[package]
name = "prometeu-layer-tests"
version = "0.1.0"
edition = "2021"
publish = false
[dev-dependencies]
prometeu-bytecode = { path = "../../console/prometeu-bytecode" }
prometeu-vm = { path = "../../console/prometeu-vm" }
prometeu-hal = { path = "../../console/prometeu-hal" }
prometeu-test-support = { path = "../prometeu-test-support" }

View File

@ -0,0 +1,2 @@
// This crate exists solely to host layered integration tests under `tests/`.
// It intentionally provides no public API.

View File

@ -0,0 +1,44 @@
use prometeu_bytecode::decode_next;
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
let code = op as u16;
out.extend_from_slice(&code.to_le_bytes());
let need = op.spec().imm_bytes as usize;
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => panic!(
"immediate size mismatch for {:?}: expected {}, got {}",
op, n, bytes.len()
),
(n, None) => panic!("missing immediate for {:?}: need {} bytes", op, n),
}
}
#[test]
fn encode_decode_edge_immediates_roundtrip() {
let mut rom = Vec::new();
emit(CoreOpCode::PushI32, Some(&i32::MIN.to_le_bytes()), &mut rom);
emit(CoreOpCode::PushI32, Some(&i32::MAX.to_le_bytes()), &mut rom);
emit(CoreOpCode::FrameSync, None, &mut rom);
emit(CoreOpCode::Halt, None, &mut rom);
// Decode sequentially and validate structure + immediates
let mut pc = 0usize;
let mut seen = Vec::new();
while pc < rom.len() {
let instr = decode_next(pc, &rom).expect("decode ok");
seen.push(instr);
pc = instr.next_pc;
}
assert_eq!(seen.len(), 4);
assert_eq!(seen[0].opcode, CoreOpCode::PushI32);
assert_eq!(seen[0].imm_i32().unwrap(), i32::MIN);
assert_eq!(seen[1].opcode, CoreOpCode::PushI32);
assert_eq!(seen[1].imm_i32().unwrap(), i32::MAX);
assert_eq!(seen[2].opcode, CoreOpCode::FrameSync);
assert_eq!(seen[3].opcode, CoreOpCode::Halt);
assert_eq!(seen.last().unwrap().next_pc, rom.len());
}

View File

@ -0,0 +1,21 @@
use prometeu_bytecode::Value;
use prometeu_vm::Heap;
#[test]
fn gc_collects_unreachable_closure_but_keeps_marked() {
let mut heap = Heap::new();
// Allocate two closures with small environments
let rooted = heap.alloc_closure(1, &[Value::Int32(10)]);
let unreachable = heap.alloc_closure(2, &[Value::Int32(20)]);
// Mark only the rooted one, then sweep.
heap.mark_from_roots([rooted]);
heap.sweep();
assert!(heap.is_valid(rooted), "rooted object must remain alive after sweep");
assert!(
!heap.is_valid(unreachable),
"unreachable object must be reclaimed by GC"
);
}

View File

@ -0,0 +1,29 @@
use prometeu_bytecode::HeapRef;
use prometeu_vm::Scheduler;
#[test]
fn scheduler_wake_and_ready_order_is_deterministic() {
let mut s = Scheduler::new();
let a = HeapRef(1);
let b = HeapRef(2);
let c = HeapRef(3);
s.sleep_until(a, 5);
s.sleep_until(b, 5);
s.sleep_until(c, 6);
// Before tick 5: nothing wakes
s.wake_ready(4);
assert!(s.is_ready_empty());
// At tick 5: a then b become ready (in insertion order)
s.wake_ready(5);
assert_eq!(s.dequeue_next(), Some(a));
assert_eq!(s.dequeue_next(), Some(b));
assert!(s.is_ready_empty());
// At tick 6: c becomes ready
s.wake_ready(6);
assert_eq!(s.dequeue_next(), Some(c));
assert!(s.is_ready_empty());
}

View File

@ -0,0 +1,21 @@
use prometeu_bytecode::isa::core::CoreOpCode;
use prometeu_bytecode::FunctionMeta;
use prometeu_vm::verifier::{Verifier, VerifierError};
#[test]
fn verifier_rejects_call_closure_when_top_is_not_closure() {
// Program: PUSH_I32 7; CALL_CLOSURE argc=0; HALT
let mut rom = Vec::new();
rom.extend_from_slice(&(CoreOpCode::PushI32 as u16).to_le_bytes());
rom.extend_from_slice(&7i32.to_le_bytes());
rom.extend_from_slice(&(CoreOpCode::CallClosure as u16).to_le_bytes());
rom.extend_from_slice(&0u32.to_le_bytes()); // argc = 0
rom.extend_from_slice(&(CoreOpCode::Halt as u16).to_le_bytes());
let functions = vec![FunctionMeta { code_offset: 0, code_len: rom.len() as u32, ..Default::default() }];
match Verifier::verify(&rom, &functions) {
Err(VerifierError::NotAClosureOnCallClosure { .. }) => {}
other => panic!("expected NotAClosureOnCallClosure, got {:?}", other),
}
}

View File

@ -0,0 +1,56 @@
use prometeu_bytecode::isa::core::{CoreOpCode, CoreOpCodeSpecExt};
use prometeu_hal::{HostContext, HostReturn, NativeInterface, SyscallId};
use prometeu_vm::{BudgetReport, LogicalFrameEndingReason, VirtualMachine};
use prometeu_bytecode::Value;
fn emit(op: CoreOpCode, imm: Option<&[u8]>, out: &mut Vec<u8>) {
out.extend_from_slice(&(op as u16).to_le_bytes());
let need = op.spec().imm_bytes as usize;
match (need, imm) {
(0, None) => {}
(n, Some(bytes)) if bytes.len() == n => out.extend_from_slice(bytes),
(n, Some(bytes)) => panic!("imm size mismatch for {:?}: need {}, got {}", op, n, bytes.len()),
(n, None) => panic!("missing imm for {:?}: need {} bytes", op, n),
}
}
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(
&mut self,
_id: SyscallId,
_args: &[Value],
_ret: &mut HostReturn,
_ctx: &mut HostContext,
) -> Result<(), prometeu_hal::vm_fault::VmFault> {
Ok(())
}
}
#[test]
fn vm_executes_valid_program_in_slices() {
// Program: PUSH_I32 2; PUSH_I32 3; ADD; FRAME_SYNC; HALT
let mut rom = Vec::new();
emit(CoreOpCode::PushI32, Some(&2i32.to_le_bytes()), &mut rom);
emit(CoreOpCode::PushI32, Some(&3i32.to_le_bytes()), &mut rom);
emit(CoreOpCode::Add, None, &mut rom);
emit(CoreOpCode::FrameSync, None, &mut rom);
emit(CoreOpCode::Halt, None, &mut rom);
// Construct VM directly; assume input is already verified.
let mut vm = VirtualMachine::new(rom, vec![]);
vm.prepare_call("0");
let mut native = NoopNative;
let mut ctx = HostContext::new(None);
// First slice should stop at FRAME_SYNC deterministically.
let report: BudgetReport = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
assert_eq!(report.reason, LogicalFrameEndingReason::FrameSync);
assert!(!vm.halted);
// Second slice proceeds to HALT.
let report2: BudgetReport = vm.run_budget(100, &mut native, &mut ctx).expect("run ok");
assert_eq!(report2.reason, LogicalFrameEndingReason::Halted);
assert!(vm.halted);
}

View File

@ -1,61 +1,3 @@
# PR-8.3 — Layered Test Suite Architecture
## Briefing
Tests must be structured by layer to isolate failures.
Layers:
1. Bytecode encode/decode
2. Verifier
3. VM execution
4. GC behavior
5. Scheduler behavior
## Target
Organize tests into modules per layer.
Enforce:
* No cross-layer assumptions.
* Verifier tests do not execute VM.
* VM tests assume verified input.
Add minimal coverage per layer.
## Acceptance Checklist
* [ ] Tests grouped logically.
* [ ] No duplicated logic.
* [ ] Clear separation of concerns.
## Tests
Add representative tests per layer:
* Encode/decode edge cases.
* Verifier rejects invalid closures.
* VM executes valid programs.
* GC collects unreachable closures.
* Scheduler deterministic ordering.
## Junie Instructions
You MAY:
* Refactor test layout.
* Add missing coverage.
You MUST NOT:
* Merge unrelated layers.
* Hide verifier failures inside runtime traps.
If a test spans layers, STOP and split appropriately.
---
# PR-8.4 — No Legacy Artifacts Enforcement # PR-8.4 — No Legacy Artifacts Enforcement
## Briefing ## Briefing

View File

@ -1,15 +1,37 @@
Stress test outline (to add in PR7.9 or here if desired) Layered Test Suite Architecture (PR8.3)
Build a bytecode program with a tight loop containing arithmetic + occasional YIELD and ensure: Overview
Under tiny budgets (1n cycles), pc progression is identical across runs. - Tests are organized by runtime layer to isolate failures and clarify ownership of behavior.
What I changed and why - Location: `crates/dev/prometeu-layer-tests/tests/`
Open in editor
Layers and representative tests
Switches (simulated by alternating between two VMs) occur only
1. Bytecode encode/decode
immediately after step() completes, or - `bytecode_encode_decode.rs` — roundtrip decoding for edge immediates and structure.
2. Verifier
when run_budget returns FrameSync. - `verifier_closure_reject.rs` — rejects `CALL_CLOSURE` when TOS is not a closure.
- Rule: verifier tests never run the VM.
No observable state change occurs when forceswitching between two VMs midtick except at these safepoints. 3. VM execution
- `vm_exec_valid.rs` — executes a tiny valid program; assumes verified input.
- Rule: VM tests do not depend on the verifier; they prepare ROM directly.
4. GC behavior
- `gc_collect_unreachable.rs` — unrooted closure is reclaimed; rooted one survives.
5. Scheduler behavior
- `scheduler_determinism.rs` — deterministic FIFO ordering for sametick wakeups.
Running the layered suite
- Run just the layered tests: `cargo test -p prometeu-layer-tests`
- Run the entire workspace: `cargo test`
Separation of concerns
- No crosslayer assumptions in these tests.
- Verifier tests: no `VirtualMachine` execution.
- VM tests: assume preverified bytecode; interact via public VM APIs only.
- GC and Scheduler tests: exercise their public APIs directly, without booting the VM.
DRY helpers
- Shared utilities should live in `crates/dev/prometeu-test-support` when needed. The current minimal suite avoids duplication by using tiny local helpers.