Skip to content

Commit 14e493e

Browse files
authored
Add Code And Bindings For Generating Test Case CheckSums (#691)
## Context Test Case IDs act as a checksum for tests - they're V5 UUID generated using aspects of the test like its filepath and name. If nothing about the test changes (including the variant used when running it), the ID remains fixed ## What Does The PR Do Currently, generating the ID / Checksum for test runs is done [in the ETL when it runs](https://github.com/trunk-io/trunk/blob/5117073037075cc5f51c2bf59fe5422c8360c8fd/trunk/py/metrics_data_pipeline/trunk_etl_common/id_schema.py#L30). This PR moves that functionality over to rust bindings so that both the ETL and other processes (like our root cause AI agent) can use it ## Testing This PR adds rust tests and tests for the TS and Python bindings. Also verified this against a recently uploaded test in staging, specifically https://app.trunk-staging.io/trunk-staging-org/flaky-tests/test/3f507aef-e834-523b-a8ad-edaba6b137be?repo=trunk-io%2Ftrunk (see actual rust test in `id.rs`
1 parent 25ef0aa commit 14e493e

File tree

7 files changed

+492
-20
lines changed

7 files changed

+492
-20
lines changed

bundle/src/types.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use context::repo::RepoUrlParts;
1+
use context::{meta::id::gen_info_id, repo::RepoUrlParts};
22
#[cfg(feature = "pyo3")]
33
use pyo3::prelude::*;
44
#[cfg(feature = "pyo3")]
@@ -57,17 +57,16 @@ impl Test {
5757
}
5858

5959
pub fn set_id<T: AsRef<str>>(&mut self, org_slug: T, repo: &RepoUrlParts) {
60-
let info_id_input = [
60+
self.id = gen_info_id(
6161
org_slug.as_ref(),
6262
repo.repo_full_name().as_str(),
63-
self.file.as_deref().unwrap_or(""),
64-
self.class_name.as_deref().unwrap_or(""),
65-
&self.parent_name,
66-
&self.name,
67-
"JUNIT_TESTCASE",
68-
]
69-
.join("#");
70-
self.id = Uuid::new_v5(&Uuid::NAMESPACE_URL, info_id_input.as_bytes()).to_string()
63+
self.file.as_deref(),
64+
self.class_name.as_deref(),
65+
Some(self.parent_name.as_str()),
66+
Some(self.name.as_str()),
67+
None,
68+
"",
69+
);
7170
}
7271

7372
pub fn generate_custom_uuid<T: AsRef<str>>(&mut self, org_slug: T, repo: &RepoUrlParts, id: T) {

context-js/src/lib.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bundle::{
66
parse_meta_from_tarball as parse_tarball_meta, FileSetTestRunnerReport, VersionedBundle,
77
VersionedBundleWithBindingsReport,
88
};
9-
use context::{env, junit, repo};
9+
use context::{env, junit, meta::id::gen_info_id as gen_info_id_impl, repo};
1010
use futures::{future::Either, io::BufReader as BufReaderAsync, stream::TryStreamExt};
1111
use js_sys::Uint8Array;
1212
use prost::Message;
@@ -175,3 +175,27 @@ pub async fn parse_internal_bin_and_meta_from_tarball(
175175
versioned_bundle,
176176
})
177177
}
178+
179+
#[wasm_bindgen]
180+
// trunk-ignore(clippy/too_many_arguments)
181+
pub fn gen_info_id(
182+
org_url_slug: String,
183+
repo_full_name: String,
184+
file: Option<String>,
185+
classname: Option<String>,
186+
parent_fact_path: Option<String>,
187+
name: Option<String>,
188+
info_id: Option<String>,
189+
variant: String,
190+
) -> String {
191+
gen_info_id_impl(
192+
&org_url_slug,
193+
&repo_full_name,
194+
file.as_deref(),
195+
classname.as_deref(),
196+
parent_fact_path.as_deref(),
197+
name.as_deref(),
198+
info_id.as_deref(),
199+
&variant,
200+
)
201+
}

context-js/tests/meta.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { describe, expect, it } from "vitest";
2+
import { gen_info_id } from "../pkg/context_js";
3+
4+
describe("context-js", () => {
5+
// These tests match the tests in context/src/meta/id.rs.
6+
// While they don't need to match, it proves both the bindings and
7+
// rust code are generating the same IDs.
8+
describe("gen_info_id", () => {
9+
it("generates ID properly for trunk", () => {
10+
expect.hasAssertions();
11+
12+
const generateIdForTest = () =>
13+
gen_info_id(
14+
"example_org",
15+
"example_repo",
16+
"src/lib.rs",
17+
"ExampleClass",
18+
"parent/fact/path",
19+
"example_name",
20+
"trunk:12345",
21+
"unix",
22+
);
23+
24+
const result = generateIdForTest();
25+
26+
expect(result).toBe("4392f63c-8dc9-5cec-bbdc-e7b90c2e5a6b");
27+
28+
// Generate again to ensure it is consistent
29+
const result2 = generateIdForTest();
30+
31+
expect(result2).toBe(result);
32+
});
33+
34+
it("works properly with existing v5 UUID", () => {
35+
expect.hasAssertions();
36+
37+
const existingInfoId = "a6e84936-3ee9-57d5-b041-ae124896f654";
38+
const generateIdForTest = ({ variant = "" }: { variant?: string }) =>
39+
gen_info_id(
40+
"example_org",
41+
"example_repo",
42+
"src/lib.rs",
43+
"ExampleClass",
44+
"parent/fact/path",
45+
"example_name",
46+
existingInfoId,
47+
variant,
48+
);
49+
50+
const result = generateIdForTest({});
51+
52+
expect(result).toBe(existingInfoId);
53+
54+
// Generate again to ensure it is consistent
55+
const result2 = generateIdForTest({});
56+
57+
expect(result2).toBe(result);
58+
59+
// Adding a variant changs the ID.
60+
const resultWithVariant = generateIdForTest({ variant: "unix" });
61+
62+
expect(resultWithVariant).toBe("8057218b-95e4-5373-afbe-c366d4058615");
63+
});
64+
65+
it("works properly without existing v5 UUID", () => {
66+
expect.hasAssertions();
67+
68+
const generateIdForTest = ({ infoId }: { infoId?: string }) =>
69+
gen_info_id(
70+
"example_org",
71+
"example_repo",
72+
"src/lib.rs",
73+
"ExampleClass",
74+
"parent/fact/path",
75+
"example_name",
76+
infoId,
77+
"unix",
78+
);
79+
80+
const result = generateIdForTest({});
81+
82+
expect(result).toBe("c869cb93-66e2-516d-a0ea-15ff4b413c3f");
83+
84+
// Generate again to ensure it is consistent
85+
const result2 = generateIdForTest({});
86+
87+
expect(result2).toBe(result);
88+
89+
// Existing UUID is ignored if it isn't V5
90+
const resultForV4Uuid = generateIdForTest({
91+
infoId: "08e1c642-3a55-45cf-8bf9-b9d0b21785dd", // V4
92+
});
93+
94+
expect(resultForV4Uuid).toBe(result);
95+
});
96+
});
97+
});

context-py/src/lib.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ use codeowners::{
1010
use context::{
1111
env,
1212
junit::{self, junit_path::TestRunnerReport},
13-
meta, repo,
13+
meta::{bindings, id, validator},
14+
repo,
1415
};
1516
use prost::Message;
1617
use pyo3::{exceptions::PyTypeError, prelude::*};
@@ -209,16 +210,14 @@ pub fn parse_meta(meta_bytes: Vec<u8>) -> PyResult<bundle::BindingsVersionedBund
209210

210211
#[gen_stub_pyfunction]
211212
#[pyfunction]
212-
fn meta_validate(
213-
meta_context: meta::bindings::BindingsMetaContext,
214-
) -> meta::validator::MetaValidation {
215-
meta::validator::validate(&meta::bindings::BindingsMetaContext::into(meta_context))
213+
fn meta_validate(meta_context: bindings::BindingsMetaContext) -> validator::MetaValidation {
214+
validator::validate(&bindings::BindingsMetaContext::into(meta_context))
216215
}
217216

218217
#[gen_stub_pyfunction]
219218
#[pyfunction]
220219
fn meta_validation_level_to_string(
221-
meta_validation_level: meta::validator::MetaValidationLevel,
220+
meta_validation_level: validator::MetaValidationLevel,
222221
) -> String {
223222
meta_validation_level.to_string()
224223
}
@@ -362,6 +361,32 @@ fn associate_codeowners_multithreaded_impl(
362361
Ok(results)
363362
}
364363

364+
#[gen_stub_pyfunction]
365+
#[pyfunction]
366+
// trunk-ignore(clippy/too_many_arguments)
367+
// trunk-ignore(clippy/deprecated)
368+
pub fn gen_info_id(
369+
org_url_slug: String,
370+
repo_full_name: String,
371+
variant: String,
372+
file: Option<String>,
373+
classname: Option<String>,
374+
parent_fact_path: Option<String>,
375+
name: Option<String>,
376+
info_id: Option<String>,
377+
) -> String {
378+
id::gen_info_id(
379+
&org_url_slug,
380+
&repo_full_name,
381+
file.as_deref(),
382+
classname.as_deref(),
383+
parent_fact_path.as_deref(),
384+
name.as_deref(),
385+
info_id.as_deref(),
386+
&variant,
387+
)
388+
}
389+
365390
#[pymodule]
366391
fn context_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
367392
m.add_class::<env::parser::CIInfo>()?;
@@ -402,9 +427,9 @@ fn context_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
402427
m.add_function(wrap_pyfunction!(repo_validate, m)?)?;
403428
m.add_function(wrap_pyfunction!(repo_validation_level_to_string, m)?)?;
404429

405-
m.add_class::<meta::bindings::BindingsMetaContext>()?;
406-
m.add_class::<meta::validator::MetaValidation>()?;
407-
m.add_class::<meta::validator::MetaValidationLevel>()?;
430+
m.add_class::<bindings::BindingsMetaContext>()?;
431+
m.add_class::<validator::MetaValidation>()?;
432+
m.add_class::<validator::MetaValidationLevel>()?;
408433
m.add_function(wrap_pyfunction!(parse_meta_from_tarball, m)?)?;
409434
m.add_function(wrap_pyfunction!(parse_meta, m)?)?;
410435
m.add_class::<bundle::BindingsVersionedBundle>()?;
@@ -417,6 +442,7 @@ fn context_py(m: &Bound<'_, PyModule>) -> PyResult<()> {
417442
m.add_function(wrap_pyfunction!(meta_validate, m)?)?;
418443
m.add_function(wrap_pyfunction!(meta_validation_level_to_string, m)?)?;
419444
m.add_function(wrap_pyfunction!(parse_internal_bin_from_tarball, m)?)?;
445+
m.add_function(wrap_pyfunction!(gen_info_id, m)?)?;
420446

421447
m.add_class::<codeowners::BindingsOwners>()?;
422448
m.add_function(wrap_pyfunction!(codeowners_parse, m)?)?;

context-py/tests/test_meta.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
from context_py import gen_info_id
2+
3+
4+
def test_generates_id_properly_for_trunk():
5+
def generate_id_for_test():
6+
return gen_info_id(
7+
"example_org",
8+
"example_repo",
9+
"unix",
10+
"src/lib.rs",
11+
"ExampleClass",
12+
"parent/fact/path",
13+
"example_name",
14+
"trunk:12345",
15+
)
16+
17+
result = generate_id_for_test()
18+
assert result == "4392f63c-8dc9-5cec-bbdc-e7b90c2e5a6b"
19+
20+
# Generate again to ensure it is consistent
21+
result2 = generate_id_for_test()
22+
assert result2 == result
23+
24+
25+
def test_works_properly_with_existing_v5_uuid():
26+
existing_info_id = "a6e84936-3ee9-57d5-b041-ae124896f654"
27+
28+
def generate_id_for_test(variant: str = ""):
29+
return gen_info_id(
30+
"example_org",
31+
"example_repo",
32+
variant,
33+
"src/lib.rs",
34+
"ExampleClass",
35+
"parent/fact/path",
36+
"example_name",
37+
existing_info_id,
38+
)
39+
40+
result = generate_id_for_test()
41+
assert result == existing_info_id
42+
43+
# Generate again to ensure it is consistent
44+
result2 = generate_id_for_test()
45+
assert result2 == result
46+
47+
# Adding a variant changes the ID
48+
result_with_variant = generate_id_for_test(variant="unix")
49+
assert result_with_variant == "8057218b-95e4-5373-afbe-c366d4058615"
50+
51+
52+
def test_works_properly_without_existing_v5_uuid():
53+
def generate_id_for_test(info_id: str | None = None):
54+
return gen_info_id(
55+
"example_org",
56+
"example_repo",
57+
"unix",
58+
"src/lib.rs",
59+
"ExampleClass",
60+
"parent/fact/path",
61+
"example_name",
62+
info_id,
63+
)
64+
65+
result = generate_id_for_test()
66+
assert result == "c869cb93-66e2-516d-a0ea-15ff4b413c3f"
67+
68+
# Generate again to ensure it is consistent
69+
result2 = generate_id_for_test()
70+
assert result2 == result
71+
72+
# Existing UUID is ignored if it isn't V5
73+
result_for_v4_uuid = generate_id_for_test(
74+
info_id="08e1c642-3a55-45cf-8bf9-b9d0b21785dd"
75+
) # V4
76+
assert result_for_v4_uuid == result

0 commit comments

Comments
 (0)