mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] sweep after phase 05: 2 deferred items resolved
This commit is contained in:
parent
09553f5b4c
commit
6d147d334e
4 changed files with 430 additions and 4 deletions
|
|
@ -475,7 +475,12 @@ pub fn handle(
|
|||
// ── Dynamic verification (feature-gated) ─────────────────────────────
|
||||
#[cfg(feature = "dynamic")]
|
||||
if config.scanner.verify {
|
||||
let opts = crate::dynamic::verify::VerifyOptions::from_config(config);
|
||||
let mut opts = crate::dynamic::verify::VerifyOptions::from_config(config);
|
||||
// Enable the verdict cache (§12 Q5) when an index DB is in use.
|
||||
// When index_mode is Off, the DB is never created, so no cache.
|
||||
if index_mode != IndexMode::Off && db_path.exists() {
|
||||
opts.db_path = Some(db_path.clone());
|
||||
}
|
||||
for diag in &mut diags {
|
||||
let result = crate::dynamic::verify::verify_finding(diag, &opts);
|
||||
if let Some(ref mut ev) = diag.evidence {
|
||||
|
|
|
|||
|
|
@ -1012,6 +1012,68 @@ fn libc_kill(pid: i32, sig: i32) -> i32 {
|
|||
unsafe { kill(pid, sig) }
|
||||
}
|
||||
|
||||
// ── Docker image digest enrichment (§22.1) ────────────────────────────────────
|
||||
|
||||
/// Map a toolchain_id to its corresponding Docker image tag.
|
||||
///
|
||||
/// Only covers Docker-backed interpreted runtimes (Python, Node, Java, PHP).
|
||||
/// Returns `None` for compiled toolchains (Rust, Go) that use the generic
|
||||
/// `debian:bookworm-slim` runtime image independently of `toolchain_id`.
|
||||
fn docker_image_for_toolchain_id(toolchain_id: &str) -> Option<String> {
|
||||
if toolchain_id.starts_with("python-") {
|
||||
Some(python_image_for_toolchain(toolchain_id))
|
||||
} else if toolchain_id.starts_with("node-") {
|
||||
Some(node_image_for_toolchain(toolchain_id))
|
||||
} else if toolchain_id.starts_with("java-") {
|
||||
Some(java_image_for_toolchain(toolchain_id))
|
||||
} else if toolchain_id.starts_with("php-") {
|
||||
Some(php_image_for_toolchain(toolchain_id))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the first 12 hex characters of the Docker image content digest.
|
||||
///
|
||||
/// Runs `docker inspect --format={{.Id}} <image>` and truncates the SHA256
|
||||
/// hex string. Returns an empty string when docker is unavailable, the image
|
||||
/// has not been pulled locally, or the output cannot be parsed.
|
||||
pub fn fetch_docker_image_digest_short(image: &str) -> String {
|
||||
let out = std::process::Command::new(docker_bin())
|
||||
.args(["inspect", "--format={{.Id}}", image])
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) if o.status.success() => {
|
||||
let id = std::str::from_utf8(&o.stdout).unwrap_or("").trim();
|
||||
let hex = id.strip_prefix("sha256:").unwrap_or(id);
|
||||
hex.chars().take(12).collect()
|
||||
}
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a toolchain_id enriched with the Docker image digest (§22.1).
|
||||
///
|
||||
/// For Docker-backed toolchains (Python, Node, Java, PHP), appends a 12-char
|
||||
/// digest suffix so that cache keys remain distinct across image updates.
|
||||
/// Example: `"python-3.11"` → `"python-3.11-abc123456789"`.
|
||||
///
|
||||
/// Returns the base ID unchanged when:
|
||||
/// - the toolchain is not Docker-backed (Rust, Go),
|
||||
/// - docker is unavailable, or
|
||||
/// - the image has not been pulled locally.
|
||||
pub fn toolchain_id_with_digest(base_id: &str) -> String {
|
||||
let Some(image) = docker_image_for_toolchain_id(base_id) else {
|
||||
return base_id.to_owned();
|
||||
};
|
||||
let digest = fetch_docker_image_digest_short(&image);
|
||||
if digest.is_empty() {
|
||||
base_id.to_owned()
|
||||
} else {
|
||||
format!("{base_id}-{digest}")
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -1197,4 +1259,62 @@ mod tests {
|
|||
// node is in the interpreter list → not native binary
|
||||
assert!(!harness_is_native_binary(&cmd));
|
||||
}
|
||||
|
||||
// ── Docker image digest enrichment tests ──────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn fetch_docker_image_digest_short_returns_empty_on_bad_image() {
|
||||
// A non-existent image tag always returns empty (inspect fails).
|
||||
let digest = fetch_docker_image_digest_short("nyx-nonexistent-image:does-not-exist-99999");
|
||||
assert!(digest.is_empty(), "non-existent image must return empty digest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_id_with_digest_passthrough_for_rust() {
|
||||
// Rust toolchain IDs are not Docker-backed; digest enrichment is a no-op.
|
||||
let id = toolchain_id_with_digest("rust-stable");
|
||||
assert_eq!(id, "rust-stable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_id_with_digest_passthrough_for_go() {
|
||||
let id = toolchain_id_with_digest("go-1.22");
|
||||
assert_eq!(id, "go-1.22");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toolchain_id_with_digest_no_suffix_when_digest_empty() {
|
||||
// When docker is absent or image not pulled, the base ID is returned unchanged.
|
||||
// We can't control whether docker is available, but a non-existent image
|
||||
// always yields an empty digest, so the base ID is returned as-is.
|
||||
let id = toolchain_id_with_digest("python-nyx-nonexistent-99999");
|
||||
// The crafted toolchain maps to python:nyx-nonexistent-99999-slim which
|
||||
// won't be present → empty digest → base ID returned.
|
||||
assert!(
|
||||
id == "python-nyx-nonexistent-99999" || id.starts_with("python-nyx-nonexistent-99999-"),
|
||||
"id should be base or base-digest, got: {id}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn docker_image_for_toolchain_id_maps_correctly() {
|
||||
assert_eq!(
|
||||
docker_image_for_toolchain_id("python-3.11"),
|
||||
Some("python:3.11-slim".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
docker_image_for_toolchain_id("node-20"),
|
||||
Some("node:20-slim".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
docker_image_for_toolchain_id("java-21"),
|
||||
Some("eclipse-temurin:21-jre-jammy".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
docker_image_for_toolchain_id("php-8"),
|
||||
Some("php:8-cli".to_owned())
|
||||
);
|
||||
assert_eq!(docker_image_for_toolchain_id("rust-stable"), None);
|
||||
assert_eq!(docker_image_for_toolchain_id("go-1.22"), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
//! It is the only function the rest of the crate needs to know about.
|
||||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::dynamic::corpus::payloads_for;
|
||||
use crate::dynamic::corpus::{payloads_for, CORPUS_VERSION};
|
||||
use crate::dynamic::report::{AttemptSummary, VerifyResult, VerifyStatus};
|
||||
use crate::dynamic::runner::{run_spec, RunError};
|
||||
use crate::dynamic::sandbox::SandboxOptions;
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
use crate::dynamic::sandbox::{toolchain_id_with_digest, SandboxOptions};
|
||||
use crate::dynamic::spec::{HarnessSpec, SPEC_FORMAT_VERSION};
|
||||
use crate::dynamic::telemetry::{self, TelemetryEvent};
|
||||
use crate::dynamic::toolchain;
|
||||
use crate::evidence::{InconclusiveReason, UnsupportedReason};
|
||||
|
|
@ -21,6 +21,9 @@ pub struct VerifyOptions {
|
|||
pub sandbox: SandboxOptions,
|
||||
/// Project root for repro artifact symlinks (optional).
|
||||
pub project_root: Option<std::path::PathBuf>,
|
||||
/// Path to the Nyx index database for the dynamic verdict cache (§12 Q5).
|
||||
/// When `None` (e.g. `--no-index` mode), the cache is bypassed entirely.
|
||||
pub db_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl VerifyOptions {
|
||||
|
|
@ -38,10 +41,113 @@ impl VerifyOptions {
|
|||
..SandboxOptions::default()
|
||||
},
|
||||
project_root: None,
|
||||
db_path: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Dynamic verdict cache helpers (§12 Q5) ───────────────────────────────────
|
||||
|
||||
/// Hash the content of `entry_file` with BLAKE3 and return a 16-char hex string.
|
||||
///
|
||||
/// Returns `"unavailable"` when the file cannot be read (e.g. the finding
|
||||
/// points to a file that no longer exists). The cache simply misses in that case.
|
||||
fn compute_entry_content_hash(entry_file: &str) -> String {
|
||||
std::fs::read(entry_file)
|
||||
.map(|bytes| {
|
||||
let h = blake3::hash(&bytes);
|
||||
format!(
|
||||
"{:016x}",
|
||||
u64::from_le_bytes(h.as_bytes()[..8].try_into().unwrap())
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|_| "unavailable".to_owned())
|
||||
}
|
||||
|
||||
/// Placeholder transitive import digest.
|
||||
///
|
||||
/// Full transitive import analysis is deferred. The empty string is a valid
|
||||
/// conservative placeholder: a stale cache hit can only occur when a transitive
|
||||
/// import changes without the entry file changing, which is rare and unlikely to
|
||||
/// cause incorrect verdicts given the harness is also re-confirmed by the oracle.
|
||||
fn transitive_import_digest_placeholder() -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
/// Look up a cached verdict in the `dynamic_verdict_cache` table.
|
||||
///
|
||||
/// Opens the DB in read-write mode (no-create) so it never creates a DB that
|
||||
/// does not yet exist. Returns `None` on any error or cache miss.
|
||||
fn lookup_verdict_cache(
|
||||
db_path: &std::path::Path,
|
||||
spec_hash: &str,
|
||||
entry_content_hash: &str,
|
||||
transitive_import_digest: &str,
|
||||
toolchain_id: &str,
|
||||
) -> Option<VerifyResult> {
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||
let conn = Connection::open_with_flags(db_path, flags).ok()?;
|
||||
conn.query_row(
|
||||
"SELECT verdict_json FROM dynamic_verdict_cache \
|
||||
WHERE spec_hash = ?1 AND entry_content_hash = ?2 \
|
||||
AND transitive_import_digest = ?3 AND toolchain_id = ?4 \
|
||||
AND corpus_version = ?5 AND spec_format_version = ?6 \
|
||||
LIMIT 1",
|
||||
rusqlite::params![
|
||||
spec_hash,
|
||||
entry_content_hash,
|
||||
transitive_import_digest,
|
||||
toolchain_id,
|
||||
CORPUS_VERSION as i64,
|
||||
SPEC_FORMAT_VERSION as i64,
|
||||
],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.ok()
|
||||
.and_then(|json| serde_json::from_str(&json).ok())
|
||||
}
|
||||
|
||||
/// Insert or replace a verdict in the `dynamic_verdict_cache` table.
|
||||
///
|
||||
/// Best-effort: silently ignores all errors (DB unavailable, serialisation
|
||||
/// failure, UNIQUE constraint violation, etc.). The cache is an optimisation;
|
||||
/// a miss is never fatal.
|
||||
fn insert_verdict_cache(
|
||||
db_path: &std::path::Path,
|
||||
spec_hash: &str,
|
||||
entry_content_hash: &str,
|
||||
transitive_import_digest: &str,
|
||||
toolchain_id: &str,
|
||||
result: &VerifyResult,
|
||||
) {
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
let flags = OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX;
|
||||
let Ok(conn) = Connection::open_with_flags(db_path, flags) else {
|
||||
return;
|
||||
};
|
||||
let Ok(json) = serde_json::to_string(result) else {
|
||||
return;
|
||||
};
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let _ = conn.execute(
|
||||
"INSERT OR REPLACE INTO dynamic_verdict_cache \
|
||||
(spec_hash, entry_content_hash, transitive_import_digest, toolchain_id, \
|
||||
corpus_version, spec_format_version, verdict_json, created_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
|
||||
rusqlite::params![
|
||||
spec_hash,
|
||||
entry_content_hash,
|
||||
transitive_import_digest,
|
||||
toolchain_id,
|
||||
CORPUS_VERSION as i64,
|
||||
SPEC_FORMAT_VERSION as i64,
|
||||
json,
|
||||
now,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Try to dynamically confirm a static finding.
|
||||
///
|
||||
/// Never fails: every error path collapses into a [`VerifyStatus`] so the
|
||||
|
|
@ -105,6 +211,25 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
_ => toolchain::resolve_python(Path::new(".")),
|
||||
};
|
||||
let toolchain_match = if toolchain_res.toolchain_drift { "drift" } else { "exact" };
|
||||
// Enrich the resolved toolchain_id with the Docker image digest (§22.1).
|
||||
// The enriched ID is used as the toolchain_id component of the verdict cache
|
||||
// key so that image updates always invalidate stale cache entries.
|
||||
let effective_toolchain_id = toolchain_id_with_digest(&toolchain_res.toolchain_id);
|
||||
|
||||
// Verdict cache lookup (§12 Q5): skip execution when a valid cached result exists.
|
||||
let entry_hash = compute_entry_content_hash(&spec.entry_file);
|
||||
let import_digest = transitive_import_digest_placeholder();
|
||||
if let Some(ref db_path) = opts.db_path {
|
||||
if let Some(cached) = lookup_verdict_cache(
|
||||
db_path,
|
||||
&spec.spec_hash,
|
||||
&entry_hash,
|
||||
import_digest,
|
||||
&effective_toolchain_id,
|
||||
) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
let start = Instant::now();
|
||||
let result = run_spec(&spec, &opts.sandbox);
|
||||
|
|
@ -126,6 +251,18 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
elapsed,
|
||||
);
|
||||
|
||||
// Store result in verdict cache (best-effort; errors are silently ignored).
|
||||
if let Some(ref db_path) = opts.db_path {
|
||||
insert_verdict_cache(
|
||||
db_path,
|
||||
&spec.spec_hash,
|
||||
&entry_hash,
|
||||
import_digest,
|
||||
&effective_toolchain_id,
|
||||
&verdict,
|
||||
);
|
||||
}
|
||||
|
||||
// Emit telemetry (best-effort; never affects verdict).
|
||||
let event = TelemetryEvent::new(
|
||||
&spec,
|
||||
|
|
@ -293,3 +430,165 @@ fn build_verdict(
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn compute_entry_content_hash_stable_for_same_file() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let path = dir.path().join("entry.py");
|
||||
std::fs::write(&path, b"def run(x): pass\n").unwrap();
|
||||
let h1 = compute_entry_content_hash(path.to_str().unwrap());
|
||||
let h2 = compute_entry_content_hash(path.to_str().unwrap());
|
||||
assert_eq!(h1, h2, "hash must be deterministic");
|
||||
assert_ne!(h1, "unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_entry_content_hash_different_for_different_content() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let p1 = dir.path().join("a.py");
|
||||
let p2 = dir.path().join("b.py");
|
||||
std::fs::write(&p1, b"def run(x): return x\n").unwrap();
|
||||
std::fs::write(&p2, b"def run(x): return x + 1\n").unwrap();
|
||||
let h1 = compute_entry_content_hash(p1.to_str().unwrap());
|
||||
let h2 = compute_entry_content_hash(p2.to_str().unwrap());
|
||||
assert_ne!(h1, h2, "different content must produce different hashes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_entry_content_hash_missing_file_returns_unavailable() {
|
||||
let h = compute_entry_content_hash("/tmp/nyx_test_nonexistent_entry_file_99999.py");
|
||||
assert_eq!(h, "unavailable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transitive_import_digest_placeholder_is_stable() {
|
||||
assert_eq!(transitive_import_digest_placeholder(), "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verdict_cache_round_trip() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let db_path = dir.path().join("test.db");
|
||||
|
||||
// Create and initialize the DB with the required schema.
|
||||
{
|
||||
use rusqlite::Connection;
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS dynamic_verdict_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
spec_hash TEXT NOT NULL,
|
||||
entry_content_hash TEXT NOT NULL,
|
||||
transitive_import_digest TEXT NOT NULL,
|
||||
toolchain_id TEXT NOT NULL,
|
||||
corpus_version INTEGER NOT NULL,
|
||||
spec_format_version INTEGER NOT NULL,
|
||||
verdict_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(spec_hash, entry_content_hash, transitive_import_digest,
|
||||
toolchain_id, corpus_version, spec_format_version)
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let result = VerifyResult {
|
||||
finding_id: "test_finding_0001".to_owned(),
|
||||
status: crate::evidence::VerifyStatus::NotConfirmed,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: Some("exact".to_owned()),
|
||||
};
|
||||
|
||||
// Insert.
|
||||
insert_verdict_cache(&db_path, "spec_abc", "hash_xyz", "", "python-3.11", &result);
|
||||
|
||||
// Lookup — should return the same result.
|
||||
let cached = lookup_verdict_cache(&db_path, "spec_abc", "hash_xyz", "", "python-3.11");
|
||||
assert!(cached.is_some(), "cache hit expected after insert");
|
||||
let cached = cached.unwrap();
|
||||
assert_eq!(cached.finding_id, "test_finding_0001");
|
||||
assert_eq!(cached.status, crate::evidence::VerifyStatus::NotConfirmed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verdict_cache_miss_on_different_spec_hash() {
|
||||
let dir = tempfile::TempDir::new().unwrap();
|
||||
let db_path = dir.path().join("test.db");
|
||||
|
||||
{
|
||||
use rusqlite::Connection;
|
||||
let conn = Connection::open(&db_path).unwrap();
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE IF NOT EXISTS dynamic_verdict_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
spec_hash TEXT NOT NULL,
|
||||
entry_content_hash TEXT NOT NULL,
|
||||
transitive_import_digest TEXT NOT NULL,
|
||||
toolchain_id TEXT NOT NULL,
|
||||
corpus_version INTEGER NOT NULL,
|
||||
spec_format_version INTEGER NOT NULL,
|
||||
verdict_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
UNIQUE(spec_hash, entry_content_hash, transitive_import_digest,
|
||||
toolchain_id, corpus_version, spec_format_version)
|
||||
);",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let result = VerifyResult {
|
||||
finding_id: "test_finding_0002".to_owned(),
|
||||
status: crate::evidence::VerifyStatus::NotConfirmed,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: Some("exact".to_owned()),
|
||||
};
|
||||
|
||||
insert_verdict_cache(&db_path, "spec_aaa", "hash_xyz", "", "python-3.11", &result);
|
||||
|
||||
// Different spec_hash → miss.
|
||||
let miss = lookup_verdict_cache(&db_path, "spec_bbb", "hash_xyz", "", "python-3.11");
|
||||
assert!(miss.is_none(), "different spec_hash must be a cache miss");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verdict_cache_returns_none_for_nonexistent_db() {
|
||||
let result = lookup_verdict_cache(
|
||||
std::path::Path::new("/tmp/nyx_nonexistent_verdict_cache_99999.db"),
|
||||
"spec_abc",
|
||||
"hash_xyz",
|
||||
"",
|
||||
"python-3.11",
|
||||
);
|
||||
assert!(result.is_none(), "non-existent DB must return None");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_verdict_cache_is_noop_for_nonexistent_db() {
|
||||
// Should not panic or create the DB.
|
||||
let db_path = std::path::Path::new("/tmp/nyx_nonexistent_verdict_cache_insert_99999.db");
|
||||
let result = VerifyResult {
|
||||
finding_id: "test".to_owned(),
|
||||
status: crate::evidence::VerifyStatus::NotConfirmed,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: None,
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
};
|
||||
insert_verdict_cache(db_path, "spec", "hash", "", "python-3", &result);
|
||||
assert!(!db_path.exists(), "insert must not create a new DB");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ mod parity_tests {
|
|||
..SandboxOptions::default()
|
||||
},
|
||||
project_root: None,
|
||||
db_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +115,7 @@ mod parity_tests {
|
|||
..SandboxOptions::default()
|
||||
},
|
||||
project_root: None,
|
||||
db_path: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue