Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 7 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ criterion = "0.8"
css-inline = { version = "0.20", default-features = false }
dashmap = "6.1"
derive_more = { version = "2.1", features = ["display"] }
diesel = { version = "2.3", default-features = false, features = ["r2d2", "with-deprecated"] }
diesel = { version = "2.3", default-features = false, features = ["r2d2", "serde_json", "with-deprecated"] }
diesel_migrations = "2.3"
dotenvy = "0.15"
email_address = "0.2"
Expand Down Expand Up @@ -155,13 +155,18 @@ rmcp = { version = "2.2", default-features = false }
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs"] }
rusqlite = "0.38"
rust_decimal = "1.40"
# ECMAScript `Number::toString` formatting, which is what RFC 8785 (JCS) requires
ryu-js = "1.0"
schemars = { version = "0.8", features = ["uuid1"] }
sentry = { version = "0.48", default-features = false, features = [
"reqwest",
"rustls-no-provider",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# float_roundtrip: correctly rounded float parsing. Without it parsing can be a
# unit in the last place off, which makes an RFC 8785 canonical form unstable
# across a parse and re-serialize round trip.
serde_json = { version = "1.0", features = ["float_roundtrip"] }
serde_urlencoded = "0.7"
serde_yaml = "0.9"
sha2 = "0.11"
Expand Down
138 changes: 136 additions & 2 deletions lib/api_projects/tests/benchmarks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
)]
//! Integration tests for project benchmark endpoints.

use bencher_api_tests::TestServer;
use bencher_json::JsonBenchmarks;
use bencher_api_tests::{TestServer, helpers::create_empty_parameter};
use bencher_json::{JsonBenchmark, JsonBenchmarks, JsonParameters};
use bencher_schema::schema;
use diesel::{
ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _, connection::SimpleConnection as _,
};
use http::StatusCode;

// GET /v0/projects/{project}/benchmarks - list benchmarks (empty)
Expand Down Expand Up @@ -129,3 +133,133 @@ async fn benchmarks_delete_not_found() {

assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

// Every benchmark is born with exactly one empty parameter set, so no benchmark
// row may exist without one.
#[expect(clippy::expect_used, reason = "test assertion")]
fn assert_benchmark_birth_invariant(server: &TestServer) {
let mut conn = server.db_conn();
let benchmark_ids: Vec<i32> = schema::benchmark::table
.select(schema::benchmark::id)
.load(&mut conn)
.expect("Failed to load benchmarks");
assert!(
!benchmark_ids.is_empty(),
"expected at least one benchmark to check"
);

for benchmark_id in benchmark_ids {
let parameters: Vec<JsonParameters> = schema::parameter::table
.filter(schema::parameter::benchmark_id.eq(benchmark_id))
.select(schema::parameter::parameters)
.load(&mut conn)
.expect("Failed to load parameters");
assert_eq!(
parameters,
vec![JsonParameters::default()],
"benchmark {benchmark_id} must have exactly one empty parameter set"
);
}
}

// POST /v0/projects/{project}/benchmarks - create with the empty parameter set
#[tokio::test]
async fn benchmarks_create_empty_parameter_set() {
let server = TestServer::new().await;
let user = server
.signup("Test User", "benchmarkparameter@example.com")
.await;
let org = server.create_org(&user, "Benchmark Parameter Org").await;
let project = server
.create_project(&user, &org, "Benchmark Parameter Project")
.await;

let project_slug: &str = project.slug.as_ref();
let resp = server
.client
.post(server.api_url(&format!("/v0/projects/{}/benchmarks", project_slug)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&user.token),
)
.json(&serde_json::json!({ "name": "bench one" }))
.send()
.await
.expect("Request failed");

assert_eq!(resp.status(), StatusCode::CREATED);
let benchmark: JsonBenchmark = resp.json().await.expect("Failed to parse response");

let mut conn = server.db_conn();
let benchmark_id: i32 = schema::benchmark::table
.filter(schema::benchmark::uuid.eq(benchmark.uuid))
.select(schema::benchmark::id)
.first(&mut conn)
.expect("Failed to get benchmark ID");
let parameters: Vec<JsonParameters> = schema::parameter::table
.filter(schema::parameter::benchmark_id.eq(benchmark_id))
.select(schema::parameter::parameters)
.load(&mut conn)
.expect("Failed to load parameters");
assert_eq!(parameters, vec![JsonParameters::default()]);

assert_benchmark_birth_invariant(&server);
}

// POST /v0/projects/{project}/benchmarks - the benchmark insert rolls back with
// the empty parameter set insert
#[tokio::test]
async fn benchmarks_create_rolls_back_with_parameter_set() {
let server = TestServer::new().await;
let user = server
.signup("Test User", "benchmarkrollback@example.com")
.await;
let org = server.create_org(&user, "Benchmark Rollback Org").await;
let project = server
.create_project(&user, &org, "Benchmark Rollback Project")
.await;

// Poison the empty parameter set that the next benchmark will be born with.
// SQLite hands an `INTEGER PRIMARY KEY` the next rowid after the largest in
// use, so the row below collides on `UNIQUE(benchmark_id, parameters)` with
// the set created inside the benchmark's own transaction. Foreign keys are
// off on this connection, so it may point at a benchmark that does not exist yet.
let mut conn = server.db_conn();
let largest_benchmark_id: Option<i32> = schema::benchmark::table
.select(diesel::dsl::max(schema::benchmark::id))
.first(&mut conn)
.expect("Failed to get the largest benchmark ID");
let next_benchmark_id = largest_benchmark_id.unwrap_or_default() + 1;
conn.batch_execute("PRAGMA foreign_keys = OFF")
.expect("Failed to disable foreign keys");
create_empty_parameter(&mut conn, next_benchmark_id);
conn.batch_execute("PRAGMA foreign_keys = ON")
.expect("Failed to enable foreign keys");

let project_slug: &str = project.slug.as_ref();
let resp = server
.client
.post(server.api_url(&format!("/v0/projects/{}/benchmarks", project_slug)))
.header(
bencher_json::AUTHORIZATION,
bencher_json::bearer_header(&user.token),
)
.json(&serde_json::json!({ "name": "bench one" }))
.send()
.await
.expect("Request failed");
assert!(
!resp.status().is_success(),
"creating a benchmark whose parameter set collides must fail"
);

let benchmarks: i64 = schema::benchmark::table
.filter(schema::benchmark::name.eq("bench one"))
.count()
.get_result(&mut conn)
.expect("Failed to count benchmarks");
assert_eq!(
benchmarks, 0,
"the benchmark insert must roll back with its parameter set insert"
);
}
4 changes: 3 additions & 1 deletion lib/api_projects/tests/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use bencher_api_tests::{
TestServer,
helpers::{base_timestamp, create_test_report, get_project_id},
helpers::{base_timestamp, create_empty_parameter, create_test_report, get_project_id},
};
use bencher_json::{BenchmarkUuid, JsonOneMetric, MeasureUuid, MetricUuid, ReportBenchmarkUuid};
use bencher_schema::{
Expand Down Expand Up @@ -46,6 +46,7 @@ fn create_test_metric(server: &TestServer, project_id: i32, report_id: i32) -> M
.select(schema::benchmark::id)
.first(&mut conn)
.expect("Failed to get benchmark ID");
let parameter_id = create_empty_parameter(&mut conn, benchmark_id);

// Measure
let measure_uuid = MeasureUuid::new();
Expand Down Expand Up @@ -75,6 +76,7 @@ fn create_test_metric(server: &TestServer, project_id: i32, report_id: i32) -> M
schema::report_benchmark::report_id.eq(report_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(benchmark_id),
schema::report_benchmark::parameter_id.eq(parameter_id),
))
.execute(&mut conn)
.expect("Failed to insert report_benchmark");
Expand Down
13 changes: 12 additions & 1 deletion lib/api_projects/tests/perf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

use bencher_api_tests::{
TestServer,
helpers::{base_timestamp, get_project_id},
helpers::{base_timestamp, create_empty_parameter, get_project_id},
};
use bencher_json::{
AlertUuid, BenchmarkUuid, BoundaryUuid, BranchUuid, HeadUuid, JobStatus, JobUuid, JsonPerf,
Expand Down Expand Up @@ -44,6 +44,7 @@ struct PerfTestData {
head_id: i32,
testbed_id: i32,
benchmark_id: i32,
parameter_id: i32,
measure_id: i32,
report_id: i32,
report_benchmark_id: i32,
Expand Down Expand Up @@ -232,6 +233,7 @@ fn create_perf_data_with_options(
.select(schema::benchmark::id)
.first(&mut conn)
.expect("get benchmark id");
let parameter_id = create_empty_parameter(&mut conn, benchmark_id);

// Measure
let measure_uuid = MeasureUuid::new();
Expand Down Expand Up @@ -261,6 +263,7 @@ fn create_perf_data_with_options(
schema::report_benchmark::report_id.eq(report_id),
schema::report_benchmark::iteration.eq(opts.iteration),
schema::report_benchmark::benchmark_id.eq(benchmark_id),
schema::report_benchmark::parameter_id.eq(parameter_id),
))
.execute(&mut conn)
.expect("insert report_benchmark");
Expand Down Expand Up @@ -312,6 +315,7 @@ fn create_perf_data_with_options(
head_id,
testbed_id,
benchmark_id,
parameter_id,
measure_id,
report_id,
report_benchmark_id,
Expand Down Expand Up @@ -744,6 +748,7 @@ async fn perf_get_multiple_metrics_same_permutation() {
schema::report_benchmark::report_id.eq(report2_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(data.benchmark_id),
schema::report_benchmark::parameter_id.eq(data.parameter_id),
))
.execute(&mut conn)
.expect("insert rb2");
Expand Down Expand Up @@ -1134,6 +1139,7 @@ async fn perf_multi_benchmark_query() {
.select(schema::benchmark::id)
.first(&mut conn)
.expect("get benchmark2 id");
let parameter2_id = create_empty_parameter(&mut conn, benchmark2_id);

let report_benchmark2_uuid = ReportBenchmarkUuid::new();
diesel::insert_into(schema::report_benchmark::table)
Expand All @@ -1142,6 +1148,7 @@ async fn perf_multi_benchmark_query() {
schema::report_benchmark::report_id.eq(data.report_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(benchmark2_id),
schema::report_benchmark::parameter_id.eq(parameter2_id),
))
.execute(&mut conn)
.expect("insert report_benchmark2");
Expand Down Expand Up @@ -1921,6 +1928,7 @@ async fn perf_ordered_by_version_number() {
schema::report_benchmark::report_id.eq(report_v1_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(data_v2.benchmark_id),
schema::report_benchmark::parameter_id.eq(data_v2.parameter_id),
))
.execute(&mut conn)
.expect("insert rb v1");
Expand Down Expand Up @@ -2052,6 +2060,7 @@ async fn perf_ordered_by_start_time_within_version() {
schema::report_benchmark::report_id.eq(r_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(data.benchmark_id),
schema::report_benchmark::parameter_id.eq(data.parameter_id),
))
.execute(&mut conn)
.expect("insert rb");
Expand Down Expand Up @@ -2540,6 +2549,7 @@ async fn perf_multiple_iterations() {
schema::report_benchmark::report_id.eq(data.report_id),
schema::report_benchmark::iteration.eq(1),
schema::report_benchmark::benchmark_id.eq(data.benchmark_id),
schema::report_benchmark::parameter_id.eq(data.parameter_id),
))
.execute(&mut conn)
.expect("insert rb iter1");
Expand Down Expand Up @@ -2861,6 +2871,7 @@ async fn perf_spec_filters_results() {
schema::report_benchmark::report_id.eq(report2_id),
schema::report_benchmark::iteration.eq(0),
schema::report_benchmark::benchmark_id.eq(data1.benchmark_id),
schema::report_benchmark::parameter_id.eq(data1.parameter_id),
))
.execute(&mut conn)
.expect("insert rb2");
Expand Down
10 changes: 8 additions & 2 deletions lib/api_projects/tests/projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -710,10 +710,10 @@ struct PlotDimensions {

#[expect(clippy::expect_used, reason = "test helper seeding plot dimensions")]
fn seed_plot_dimensions(server: &TestServer, project_id: i32) -> PlotDimensions {
use bencher_api_tests::helpers::base_timestamp;
use bencher_api_tests::helpers::{base_timestamp, create_empty_parameter};
use bencher_json::{BenchmarkUuid, BranchUuid, MeasureUuid, TestbedUuid};
use bencher_schema::schema;
use diesel::{ExpressionMethods as _, RunQueryDsl as _};
use diesel::{ExpressionMethods as _, QueryDsl as _, RunQueryDsl as _};

let now = base_timestamp();
let branch1 = BranchUuid::new();
Expand Down Expand Up @@ -758,6 +758,12 @@ fn seed_plot_dimensions(server: &TestServer, project_id: i32) -> PlotDimensions
))
.execute(&mut conn)
.expect("Failed to insert benchmark");
let benchmark_id: i32 = schema::benchmark::table
.filter(schema::benchmark::uuid.eq(&benchmark))
.select(schema::benchmark::id)
.first(&mut conn)
.expect("Failed to get benchmark ID");
create_empty_parameter(&mut conn, benchmark_id);
diesel::insert_into(schema::measure::table)
.values((
schema::measure::uuid.eq(&measure),
Expand Down
Loading
Loading