app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
Drew Owens
0.9.3 (93ef8bb): attached is not ready (readiness gate, host_app_gone, lock files); a declined quit on a never-had-a-do…
8483ed6
20d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
//! ADVERSARIAL REVIEW OF 0.9.2 — pass forty-one, the merge code only.
//!
//! WHAT CHECKS OUT
//!
//! * NO PING-PONG. After a merge the record keeps `legacySha256` = the digest of
//! the INCOMING legacy bytes, not of the merged live file, so an unchanged
//! legacy file compares equal on the next start; and the record of the copy
//! that held the real name follows its own bytes beside. Pinned below by
//! starting twice: one merge, no new side files, and the second start's live
//! ledger differs from the first only by the second start's own lines.
//! * THE 0.8.x LINE FORMAT is the 0.9.x one. `at` has been `as_secs()` epoch
//! seconds since v0.3.0 and serde_json has never had `preserve_order`, so a
//! copied line is byte-identical to its original and the cross-file dedupe
//! is exact. Equal `at` from two roots sorts STABLY, live first — deterministic,
//! though second resolution cannot recover the true interleaving.
//! * A malformed or `at`-less line takes the `at` of the line before it in its
//! own file; at the head of a file that is 0 and it sorts first. Harmless.
//! * REGISTRY UNION is safe for the case asked: two `Part3` claims from two
//! eras own nothing in a third session — `owns_doc` requires
//! `d.session == session` before any title or path match, and `reconcile`
//! drops a pid whose recorded start time differs from the live one. The union
//! only carries claims forward; 0.9.1's pick already carried 0.8.9's. A claim
//! whose scratch file is gone is inert: nothing open has that path. The
//! registry is `{"documents": [...]}` and nothing else, so the union loses no
//! field.
//! * `recovery_dir_unwritable` precedes the headroom check and the dialog
//! census, and neither failure point in `recovery_dir()` creates anything;
//! the sweep's end-of-run cleanup removes an unused manifest and an empty
//! folder for any later refusal.
//! * THE HASH CAP is `<=`, so exactly 16 MiB is hashed and 16 MiB + 1 is not —
//! as documented and recorded in `comparedBy`; pinned below. A file that grows
//! past the cap between the metadata read and the hash is hashed AS IT IS NOW
//! (streamed to EOF): the digest can only disagree with the record, so the
//! race can produce "changed" and never a false "same". Safe direction.
mod common;
use common::*;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use swx_bridge::scratch::merge_ledger_lines;
fn write(path: &Path, bytes: &[u8]) {
if let Some(p) = path.parent() {
std::fs::create_dir_all(p).unwrap();
}
std::fs::write(path, bytes).unwrap();
}
fn set_mtime(path: &Path, t: SystemTime) {
std::fs::File::options().write(true).open(path).unwrap().set_modified(t).unwrap();
}
struct Layout {
local: TempDir,
}
impl Layout {
fn new(tag: &str) -> Layout {
Layout { local: TempDir::new(tag) }
}
fn data(&self) -> PathBuf {
self.local.0.join("adom-solidworks-bridge")
}
fn cache(&self) -> PathBuf {
self.local.0.join("Adom Bridge").join("bridges-cache").join("solidworks")
}
fn v090(&self) -> PathBuf {
self.local.0.join("Adom Bridge").join("solidworks-data")
}
fn env(&self) -> [(&'static str, String); 2] {
[("SWX_SCRATCH_DIR", String::new()), ("LOCALAPPDATA", self.local.path())]
}
}
fn start(lay: &Layout) -> Bridge {
let e = lay.env();
Bridge::start_with(&[(e[0].0, e[0].1.as_str()), (e[1].0, e[1].1.as_str())])
}
/// A ledger line exactly as `ledger::record_in` writes it: serde_json's default
/// map is sorted, so the keys come out `action`, `at`, `detail`.
fn line(at: u64, action: &str, detail: Value) -> String {
json!({"at": at, "action": action, "detail": detail}).to_string()
}
/// SEVERITY 2 — THE MERGE COLLAPSES REPEATED REAL EVENTS, IN THE WHOLE LIVE
/// LEDGER.
///
/// `merge_ledger_lines` dedupes through ONE `HashSet` shared across both inputs
/// AND within each. Across inputs that is the point: a line present in both
/// files is one event copied. WITHIN one file it is a loss: two identical lines
/// there are two events. A ledger line is `{"at": <epoch SECONDS>, "action",
/// "detail"}` with no sequence and no id, and several details carry nothing that
/// tells two events apart:
///
/// * `document_closed` from `swx_reset_workspace` records `{"title", "by"}` —
/// no path. Two imports of one STEP are both titled `smoke.SLDPRT` (pass 31
/// measured this), each has its own file since 0.6.0, so the reset closes
/// both, by path, in the same second: two identical lines.
/// * `parameter_set {name, value}` — an agent setting a dimension twice to the
/// same value in a second (a retry, a rebuild loop).
/// * `document_activated {name}`.
///
/// And the set is applied to the LIVE file, not just the legacy one: the first
/// time ANY legacy version has to be merged, every repeated event in the live
/// audit log since install is collapsed to one, permanently. Afterwards the
/// ledger says one document was closed where two were — about exactly the
/// action the ledger exists to record.
///
/// Fix: a multiset merge — each distinct line max(count in live, count in
/// incoming) times — which keeps copied events once and repeated events every
/// time. Going forward, a per-line sequence (or ms + pid) makes lines distinct.
#[test]
fn a_repeated_event_within_one_ledger_survives_the_merge() {
let closed = line(1_790_900_000, "document_closed", json!({"title": "smoke.SLDPRT", "by": "reset_workspace"}));
let before = line(1_790_899_000, "document_created", json!({"title": "smoke.SLDPRT"}));
// THE LIVE LEDGER: two imports of one STEP, both closed in the same second.
let live = format!("{before}\n{closed}\n{closed}\n");
// THE LEGACY VERSION: copied before the second close happened.
let incoming = format!("{before}\n{closed}\n");
let (merged, n) = merge_ledger_lines(&live, &incoming);
let closes = merged.lines().filter(|l| *l == closed).count();
assert!(n >= 2, "precondition: the merge produced a ledger: {merged:?}");
assert_eq!(
closes, 2,
"two documents were closed — the live ledger says so, twice — and the merged ledger \
records ONE. merge_ledger_lines dedupes through a single set shared within each file, \
and a ledger line has no sequence: `document_closed` from swx_reset_workspace carries \
only {{title, by}}, so two same-titled imports closed in one second are byte-identical. \
The set is applied to the LIVE file too, so the first merge collapses every repeated \
event since install. Merge as a multiset: max(count in live, count in incoming).\n \
merged: {merged:?}"
);
}
/// SEVERITY 3 — THE UNION IS CUT BACK TO 2,000 LINES BY THE NEXT WRITE ON THE
/// SAME START.
///
/// `LEDGER_MAX_LINES` is 2,000 and `record_in` drains the OLDEST lines past it.
/// The merge writes the full union — up to two caps' worth — and then, on the
/// same start, `server.rs` records `data_migrated_from_install_cache` and
/// `legacy_files_merged`. The first of those trims the live ledger back to
/// 2,000, ordered by `at`, so the oldest history in the union — mostly the older
/// era — leaves the live file at once. The migration record still says
/// `entries: <the union>`, so the record and the file disagree from the moment
/// the bridge is up. Nothing is lost on disk — both versions were copied beside
/// first — but the merge was introduced so that the LIVE ledger holds every
/// history, and past the cap it does not. Either exempt the migration start from
/// the cap, or record in the merge entry how many lines the cap removed and that
/// they are in the side copies.
#[test]
fn a_merged_ledger_is_not_truncated_by_the_cap_on_the_same_start() {
let lay = Layout::new("adv39-cap");
let mut a = String::new();
let mut b = String::new();
for i in 0..1_500u64 {
a.push_str(&line(1_780_000_000 + i, "parameter_set", json!({"name": format!("A{i}"), "value": i})));
a.push('\n');
b.push_str(&line(1_790_000_000 + i, "parameter_set", json!({"name": format!("B{i}"), "value": i})));
b.push('\n');
}
write(&lay.cache().join("ledger.jsonl"), a.as_bytes());
write(&lay.v090().join("ledger.jsonl"), b.as_bytes());
let br = start(&lay);
let _ = br.call("swx_recover_list", json!({}));
drop(br);
let live = std::fs::read_to_string(lay.data().join("ledger.jsonl")).unwrap_or_default();
assert!(live.contains("\"B1499\""), "precondition: the merge ran: {} lines", live.lines().count());
let first_era = live.lines().filter(|l| l.contains("\"name\":\"A")).count();
assert_eq!(
first_era,
1_500,
"the union of two 1,500-line ledgers was written, and the very next ledger write on the \
same start cut the live file back to LEDGER_MAX_LINES (2,000), dropping {} of the older \
era's lines. They survive only in the side copies; the merge record still counts the \
union. The merge exists so the LIVE ledger holds every history. live now has {} lines.",
1_500 - first_era,
live.lines().count()
);
}
/// PIN — THE MERGE IS IDEMPOTENT. Passes today; the coordinator's target (2).
/// Two versions of the ledger and the registry, two starts: one merge each,
/// recorded once, and no new side files on the second start.
#[test]
fn a_merge_happens_once_and_the_second_start_changes_nothing() {
let lay = Layout::new("adv39-idem");
let old = SystemTime::now() - Duration::from_secs(86_400);
write(&lay.cache().join("ledger.jsonl"), format!("{}\n", line(1_789_000_000, "document_created", json!({"title": "CACHE"}))).as_bytes());
write(&lay.v090().join("ledger.jsonl"), format!("{}\n", line(1_790_000_000, "document_created", json!({"title": "V090"}))).as_bytes());
write(&lay.cache().join("owned-docs.json"), br#"{"documents":[{"title":"Part1","path":"C:\\a\\Part1-1.SLDPRT","session":11}]}"#);
write(&lay.v090().join("owned-docs.json"), br#"{"documents":[{"title":"Part2","path":"C:\\b\\Part2-2.SLDPRT","session":22}]}"#);
for p in [lay.cache().join("ledger.jsonl"), lay.v090().join("ledger.jsonl"),
lay.cache().join("owned-docs.json"), lay.v090().join("owned-docs.json")] {
set_mtime(&p, old);
}
let side = |d: &Path| -> usize {
std::fs::read_dir(d)
.map(|rd| rd.flatten().filter(|e| e.file_name().to_string_lossy().contains(".legacy-")).count())
.unwrap_or(0)
};
let merges = |l: &str| l.lines().filter(|x| x.contains("\"action\":\"legacy_files_merged\"")).count();
let b1 = start(&lay);
let _ = b1.call("swx_recover_list", json!({}));
drop(b1);
let ledger1 = std::fs::read_to_string(lay.data().join("ledger.jsonl")).unwrap_or_default();
let reg1 = std::fs::read_to_string(lay.data().join("owned-docs.json")).unwrap_or_default();
let side1 = side(&lay.data());
assert!(ledger1.contains("CACHE") && ledger1.contains("V090"), "first start merged the ledgers: {ledger1:?}");
assert!(reg1.contains("Part1") && reg1.contains("Part2"), "first start merged the registries: {reg1:?}");
assert!(side1 > 0, "precondition: both versions were copied beside");
// NOT VACUOUS: the count compared below must start above zero, or "equal"
// would be 0 == 0 — the wrong-reason pass this review keeps catching.
assert!(merges(&ledger1) >= 1, "precondition: the merge was recorded in the ledger: {ledger1}");
let b2 = start(&lay);
let _ = b2.call("swx_recover_list", json!({}));
drop(b2);
let ledger2 = std::fs::read_to_string(lay.data().join("ledger.jsonl")).unwrap_or_default();
let reg2 = std::fs::read_to_string(lay.data().join("owned-docs.json")).unwrap_or_default();
assert_eq!(merges(&ledger1), merges(&ledger2), "the second start merged again: {ledger2}");
assert_eq!(side1, side(&lay.data()), "the second start copied more versions beside");
assert!(ledger2.starts_with(ledger1.trim_end_matches('\n')), "the second start rewrote history rather than appending");
assert_eq!(reg1, reg2, "the second start rewrote the merged registry");
}
/// PIN — THE HASH CAP AT ITS BOUNDARY. Passes today; the coordinator's target (5).
/// A same-size, same-mtime content change is CAUGHT at exactly 16 MiB (hashed)
/// and, as documented, NOT at 16 MiB + 1 (size+mtime only, `comparedBy` says so).
#[test]
fn the_hash_cap_is_inclusive_at_sixteen_mebibytes() {
use swx_bridge::scratch::MIGRATION_HASH_CAP;
let lay = Layout::new("adv39-hashcap");
let t = SystemTime::now() - Duration::from_secs(7_200);
let at = |n: u64| lay.cache().join("exports").join(format!("blob-{n}.bin"));
let sizes = [MIGRATION_HASH_CAP, MIGRATION_HASH_CAP + 1];
for &n in &sizes {
write(&at(n), &vec![b'a'; n as usize]);
set_mtime(&at(n), t);
}
{
let b = start(&lay);
let _ = b.call("swx_recover_list", json!({}));
}
// NOT VACUOUS: both blobs must have been copied on the first start, or the
// "not changed" verdict for the larger one would hold for a file never seen.
for &n in &sizes {
let copied = lay.data().join("exports").join(format!("blob-{n}.bin"));
assert_eq!(
std::fs::metadata(&copied).map(|m| m.len()).ok(),
Some(n),
"precondition: blob-{n}.bin was migrated on the first start"
);
}
for &n in &sizes {
let mut v = vec![b'a'; n as usize];
v[(n / 2) as usize] = b'b'; // same length, different bytes
write(&at(n), &v);
set_mtime(&at(n), t);
}
{
let b = start(&lay);
let _ = b.call("swx_recover_list", json!({}));
}
let marker = std::fs::read_to_string(lay.data().join("migrated-from-legacy.json")).unwrap_or_default();
let m: Value = serde_json::from_str(&marker).unwrap_or(Value::Null);
let changed: Vec<String> = m["changedSinceCopy"]
.as_array()
.map(|a| a.iter().filter_map(|e| e["file"].as_str().map(|s| s.to_string())).collect())
.unwrap_or_default();
let name = |n: u64| format!("exports/blob-{n}.bin").replace('/', std::path::MAIN_SEPARATOR_STR);
assert!(
changed.iter().any(|f| f.ends_with(&format!("blob-{}.bin", MIGRATION_HASH_CAP))),
"a same-size same-mtime change to a file of EXACTLY the cap ({} bytes) must be caught by \
the hash: changed = {changed:?}",
MIGRATION_HASH_CAP
);
assert!(
!changed.iter().any(|f| f.ends_with(&format!("blob-{}.bin", MIGRATION_HASH_CAP + 1))),
"one byte over the cap is compared by size+mtime only, as documented: changed = {changed:?}"
);
let _ = name;
}