Skip to content
Tracon

RunScoreStoreContract

Namespace Tracon.Testing.Contracts.Storage · Assembly Tracon.Testing.Contracts.Xunit.dll

Behavior tests for the IRunScoreStore contract.

public abstract class RunScoreStoreContract : TenantIsolationContract<IRunScoreStore>, IAsyncLifetime, IAsyncDisposable
Inheritance
object ← StoreCancellationContract<IRunScoreStore> ← TenantIsolationContract<IRunScoreStore> ← RunScoreStoreContract
Implements
IAsyncLifetime, IAsyncDisposable
Inherited Members
TenantIsolationContract<IRunScoreStore>.TenantA, TenantIsolationContract<IRunScoreStore>.TenantB, TenantIsolationContract<IRunScoreStore>.UseAmbientTenant(MutableTenantContext), TenantIsolationContract<IRunScoreStore>.SeedAsync(string, string, CancellationToken), TenantIsolationContract<IRunScoreStore>.ExistsAsync(string, object), TenantIsolationContract<IRunScoreStore>.CountAsync(string, CancellationToken), TenantIsolationContract<IRunScoreStore>.TryDeleteAsync(string, object), TenantIsolationContract<IRunScoreStore>.TryOverwriteAsync(string, string), TenantIsolationContract<IRunScoreStore>.CancellableReadAsync(CancellationToken), TenantIsolationContract<IRunScoreStore>.CancellableWriteAsync(CancellationToken), TenantIsolationContract<IRunScoreStore>.WroteAnythingAsync(), TenantIsolationContract<IRunScoreStore>.Tenant_cannot_read_another_tenants_record(), TenantIsolationContract<IRunScoreStore>.Tenant_reads_its_own_record(), TenantIsolationContract<IRunScoreStore>.Tenant_does_not_see_another_tenants_record_in_the_list(), TenantIsolationContract<IRunScoreStore>.Tenant_cannot_delete_another_tenants_record(), TenantIsolationContract<IRunScoreStore>.Same_name_lives_independently_across_two_tenants(), TenantIsolationContract<IRunScoreStore>.AmbientTenant, StoreCancellationContract<IRunScoreStore>.CreateStoreAsync(), StoreCancellationContract<IRunScoreStore>.InitializeAsync(), StoreCancellationContract<IRunScoreStore>.DisposeAsync(), StoreCancellationContract<IRunScoreStore>.OnDisposeAsync(), StoreCancellationContract<IRunScoreStore>.CancellableReadAsync(CancellationToken), StoreCancellationContract<IRunScoreStore>.CancellableWriteAsync(CancellationToken), StoreCancellationContract<IRunScoreStore>.WroteAnythingAsync(), StoreCancellationContract<IRunScoreStore>.Canceled_token_throws_on_read(), StoreCancellationContract<IRunScoreStore>.The_write_this_contract_checks_can_be_read_back(), StoreCancellationContract<IRunScoreStore>.Canceled_token_throws_on_write_and_leaves_no_trace(), StoreCancellationContract<IRunScoreStore>.Store, object.GetType(), object.MemberwiseClone(), object.ToString(), object.Equals(object?), object.Equals(object?, object?), object.ReferenceEquals(object?, object?), object.GetHashCode()

The in-memory store and the three SQL providers must pass the same scenarios. Critical rule: when the same author writes the same RunScore.Name onto the same target (a run or a message) a second time, the row is updated, not duplicated – a different name opens a new row, and neither rule applies when the author is empty (an anonymous setup).

protected RunScoreStoreContract()

A_categorical_score_carrying_a_numeric_value_is_REJECTED()

Section titled “ A_categorical_score_carrying_a_numeric_value_is_REJECTED()”
[Fact]
public Task A_categorical_score_carrying_a_numeric_value_is_REJECTED()

Task

[Fact]
public Task A_categorical_score_is_stored_as_text()

Task

[Fact]
public Task A_decimal_value_is_NOT_rounded()

Task

A_null_value_is_counted_but_does_not_enter_the_average()

Section titled “ A_null_value_is_counted_but_does_not_enter_the_average()”
[Fact]
public Task A_null_value_is_counted_but_does_not_enter_the_average()

Task

A_null_value_means_NO_MEASUREMENT_not_zero()

Section titled “ A_null_value_means_NO_MEASUREMENT_not_zero()”
[Fact]
public Task A_null_value_means_NO_MEASUREMENT_not_zero()

Task

A_numeric_score_carrying_a_text_value_is_REJECTED()

Section titled “ A_numeric_score_carrying_a_text_value_is_REJECTED()”
[Fact]
public Task A_numeric_score_carrying_a_text_value_is_REJECTED()

Task

A_score_with_no_evaluator_version_reads_back_as_null()

Section titled “ A_score_with_no_evaluator_version_reads_back_as_null()”
[Fact]
public Task A_score_with_no_evaluator_version_reads_back_as_null()

Task

The shape every row written before the version column existed has, and the shape every human and API score keeps. NULL means no VERSION WAS RESOLVED — it is not “version zero” and it must not make the read throw.

A_whole_number_value_is_read_back_as_a_number_not_an_integer()

Section titled “ A_whole_number_value_is_read_back_as_a_number_not_an_integer()”
[Fact]
public Task A_whole_number_value_is_read_back_as_a_number_not_an_integer()

Task

An_empty_match_returns_empty_lists_not_null()

Section titled “ An_empty_match_returns_empty_lists_not_null()”
[Fact]
public Task An_empty_match_returns_empty_lists_not_null()

Task

An_empty_string_Author_is_excluded_from_the_author_breakdown_like_no_author()

Section titled “ An_empty_string_Author_is_excluded_from_the_author_breakdown_like_no_author()”
[Fact]
public Task An_empty_string_Author_is_excluded_from_the_author_breakdown_like_no_author()

Task

An_empty_string_MessageId_is_treated_the_same_as_no_message()

Section titled “ An_empty_string_MessageId_is_treated_the_same_as_no_message()”
[Fact]
public Task An_empty_string_MessageId_is_treated_the_same_as_no_message()

Task

An_evaluator_version_AT_the_bound_is_accepted()

Section titled “ An_evaluator_version_AT_the_bound_is_accepted()”
[Fact]
public Task An_evaluator_version_AT_the_bound_is_accepted()

Task

The bound itself stays writable — an off-by-one here would reject legal versions.

[Fact]
public Task An_evaluator_version_round_trips()

Task

The real risk this case guards is an ordinal shift. A SQL store reads the row by bare ordinal, so a column slotted into the middle of the select list instead of appended to it moves every field after it. The shift is silent wherever the neighbouring types agree, which is why this asserts the other fields as well and not just the version.

[Theory]
[InlineData(new object?[] { "has space" })]
[InlineData(new object?[] { "" })]
[InlineData(new object?[] { "naïve" })]
[InlineData(new object?[] { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" })]
public Task An_illegal_name_is_REJECTED(string name)

name string

Task

An_over_long_evaluator_version_is_REJECTED()

Section titled “ An_over_long_evaluator_version_is_REJECTED()”
[Fact]
public Task An_over_long_evaluator_version_is_REJECTED()

Task

The bound is a stored-column bound, so every store enforces it, not just the SQL ones. A judge supplies this value; without the bound a third-party judge would decide how much of a customer’s table one score row occupies.

Another_tenants_score_cannot_be_deleted()

Section titled “ Another_tenants_score_cannot_be_deleted()”
[Fact]
public Task Another_tenants_score_cannot_be_deleted()

Task

[Fact]
public Task Another_tenants_score_is_not_visible()

Task

Another_tenants_score_never_enters_the_summary()

Section titled “ Another_tenants_score_never_enters_the_summary()”
[Fact]
public Task Another_tenants_score_never_enters_the_summary()

Task

Author_breakdown_excludes_scores_with_no_author()

Section titled “ Author_breakdown_excludes_scores_with_no_author()”
[Fact]
public Task Author_breakdown_excludes_scores_with_no_author()

Task

Categorical_scores_report_category_counts_not_an_average()

Section titled “ Categorical_scores_report_category_counts_not_an_average()”
[Fact]
public Task Categorical_scores_report_category_counts_not_an_average()

Task

Category_cardinality_beyond_MaxRows_is_truncated_and_reported()

Section titled “ Category_cardinality_beyond_MaxRows_is_truncated_and_reported()”
[Fact]
public Task Category_cardinality_beyond_MaxRows_is_truncated_and_reported()

Task

Concurrent_writes_of_the_SAME_name_leave_exactly_one_row()

Section titled “ Concurrent_writes_of_the_SAME_name_leave_exactly_one_row()”
[Fact]
public Task Concurrent_writes_of_the_SAME_name_leave_exactly_one_row()

Task

A uniqueness key that does not match shows up first as a race: two writers each miss the other’s row and both insert.

Concurrent_writes_with_null_and_empty_MessageId_leave_exactly_one_row()

Section titled “ Concurrent_writes_with_null_and_empty_MessageId_leave_exactly_one_row()”
[Fact]
public Task Concurrent_writes_with_null_and_empty_MessageId_leave_exactly_one_row()

Task

A NULL RunScore.MessageId and an empty ONE both mean “the whole run” (RunScoreStoreContract.Score_with_no_message_id_belongs_to_the_whole_run), so a concurrent write of one shape racing a write of the OTHER shape must still collapse to a single row – not two, one per shape.

Reports how many records the given tenant sees through the listing endpoint.

protected override ValueTask<int> CountAsync(string tenantId, CancellationToken cancellationToken)

tenantId string

The reading tenant.

cancellationToken CancellationToken

The token every store call inside the hook must be handed; see TenantIsolationContract.SeedAsync.

ValueTask<int>

The number of records seen.

Deleting_a_nonexistent_score_returns_false()

Section titled “ Deleting_a_nonexistent_score_returns_false()”
[Fact]
public Task Deleting_a_nonexistent_score_returns_false()

Task

Different_authors_score_the_same_target_independently()

Section titled “ Different_authors_score_the_same_target_independently()”
[Fact]
public Task Different_authors_score_the_same_target_independently()

Task

Different_kinds_of_the_same_name_are_not_averaged_together()

Section titled “ Different_kinds_of_the_same_name_are_not_averaged_together()”
[Fact]
public Task Different_kinds_of_the_same_name_are_not_averaged_together()

Task

Different_messages_are_scored_independently()

Section titled “ Different_messages_are_scored_independently()”
[Fact]
public Task Different_messages_are_scored_independently()

Task

Empty_author_makes_EVERY_call_open_a_new_row()

Section titled “ Empty_author_makes_EVERY_call_open_a_new_row()”
[Fact]
public Task Empty_author_makes_EVERY_call_open_a_new_row()

Task

Reports whether the record is visible from the given tenant’s viewpoint.

protected override ValueTask<bool> ExistsAsync(string tenantId, object key)

tenantId string

The reading tenant.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool>

true if the record is visible.

Hour_buckets_group_scores_within_the_same_hour()

Section titled “ Hour_buckets_group_scores_within_the_same_hour()”
[Fact]
public Task Hour_buckets_group_scores_within_the_same_hour()

Task

MessageId_is_never_a_breakdown_dimension()

Section titled “ MessageId_is_never_a_breakdown_dimension()”
[Fact]
public Task MessageId_is_never_a_breakdown_dimension()

Task

Repeated_writes_of_the_SAME_name_leave_exactly_one_row()

Section titled “ Repeated_writes_of_the_SAME_name_leave_exactly_one_row()”
[Fact]
public Task Repeated_writes_of_the_SAME_name_leave_exactly_one_row()

Task

Rescoring_with_a_NEWER_evaluator_replaces_the_stored_version()

Section titled “ Rescoring_with_a_NEWER_evaluator_replaces_the_stored_version()”
[Fact]
public Task Rescoring_with_a_NEWER_evaluator_replaces_the_stored_version()

Task

An upgraded evaluator writes the same (author, name) pair again. The row is updated, and the version has to move with it — a stale version on an updated score is exactly the confusion the column exists to remove.

Same_author_scoring_the_same_target_twice_UPDATES_the_row()

Section titled “ Same_author_scoring_the_same_target_twice_UPDATES_the_row()”
[Fact]
public Task Same_author_scoring_the_same_target_twice_UPDATES_the_row()

Task

Same_author_scoring_the_same_target_under_a_DIFFERENT_name_opens_a_new_row()

Section titled “ Same_author_scoring_the_same_target_under_a_DIFFERENT_name_opens_a_new_row()”
[Fact]
public Task Same_author_scoring_the_same_target_under_a_DIFFERENT_name_opens_a_new_row()

Task

[Fact]
public Task Score_is_deleted()

Task

Score_with_no_message_id_belongs_to_the_whole_run()

Section titled “ Score_with_no_message_id_belongs_to_the_whole_run()”
[Fact]
public Task Score_with_no_message_id_belongs_to_the_whole_run()

Task

Scores_are_grouped_into_time_buckets_and_empty_buckets_are_omitted()

Section titled “ Scores_are_grouped_into_time_buckets_and_empty_buckets_are_omitted()”
[Fact]
public Task Scores_are_grouped_into_time_buckets_and_empty_buckets_are_omitted()

Task

SeedAsync(string, string, CancellationToken)

Section titled “ SeedAsync(string, string, CancellationToken)”

Writes a sample record for the given tenant.

protected override ValueTask<object> SeedAsync(string tenantId, string name, CancellationToken cancellationToken)

tenantId string

The tenant that owns the record.

name string

The name distinguishing the record. The same name may be used by two tenants.

cancellationToken CancellationToken

The token every store call inside the hook must be handed. The cancellation contract calls this hook with a token that is already cancelled, so a hook that drops the token reports a store as sound when it is not.

ValueTask<object>

The key to use for reading the record back.

A score is attached to a run; the distinguishing key is the run id. Two tenants use the same run id: if isolation leaked, tenant B would see tenant A’s score.

Summary_can_be_read_while_a_score_is_being_written()

Section titled “ Summary_can_be_read_while_a_score_is_being_written()”
[Fact]
public Task Summary_can_be_read_while_a_score_is_being_written()

Task

Target_Message_excludes_run_level_scores()

Section titled “ Target_Message_excludes_run_level_scores()”
[Fact]
public Task Target_Message_excludes_run_level_scores()

Task

Target_Run_excludes_message_level_scores()

Section titled “ Target_Run_excludes_message_level_scores()”
[Fact]
public Task Target_Run_excludes_message_level_scores()

Task

Attempts to delete the record on behalf of the given tenant.

protected override ValueTask<bool?> TryDeleteAsync(string tenantId, object key)

tenantId string

The tenant attempting the delete.

key object

The key returned by TenantIsolationContract.SeedAsync.

ValueTask<bool?>

true if the delete succeeded, false if the record was not found; null if the store does not offer deletion.

Updating_ONE_name_leaves_the_authors_other_names_untouched()

Section titled “ Updating_ONE_name_leaves_the_authors_other_names_untouched()”
[Fact]
public Task Updating_ONE_name_leaves_the_authors_other_names_untouched()

Task

Week_buckets_start_on_Monday_and_group_the_whole_week()

Section titled “ Week_buckets_start_on_Monday_and_group_the_whole_week()”
[Fact]
public Task Week_buckets_start_on_Monday_and_group_the_whole_week()

Task

The week bucket is the riskiest of the three: each of the four implementations (three SQL dialects plus the in-memory store) computes “the preceding Monday” with a DIFFERENT formula (see RunScoreBucketing.Truncate’s remarks and each provider’s SelectRunScoreSeries query). This test is what proves they agree.

[Fact]
public Task Written_score_is_read_back()

Task