diff --git a/.gitignore b/.gitignore index d23278d6..87196554 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,7 @@ CoverageReports **/nupkgs src/Directory.Build.targets.bak.* **/bin** -**/obj** \ No newline at end of file +**/obj** +*.user +*.suo +packages/ \ No newline at end of file diff --git a/ai-pr-review.azure-pipelines.yml b/ai-pr-review.azure-pipelines.yml index 144b54e9..6bf95076 100644 --- a/ai-pr-review.azure-pipelines.yml +++ b/ai-pr-review.azure-pipelines.yml @@ -20,6 +20,23 @@ jobs: - checkout: self persistCredentials: true + - task: UseDotNet@2 + displayName: Install dotnet + inputs: + packageType: 'sdk' + version: '10.x' + includePreviewVersions: false + + - task: DotNetCoreCLI@2 + displayName: Run targeted parallel integration tests + inputs: + command: 'test' + projects: | + src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj + src/EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj + src/EfCore/EfCore.Relational.Helpers.Tests/EfCore.Relational.Helpers.Tests.csproj + arguments: '--configuration Release -- RunConfiguration.MaxCpuCount=0' + - task: GPTPullRequestReview@0 inputs: api_key: '$(open-ai-key)' diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 58779562..d98241b2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -119,7 +119,7 @@ jobs: inputs: command: "test" projects: "$(BuildParameters.TestProjects)" - arguments: '--configuration $(BuildConfiguration) --settings coverage.runsettings --collect "XPlat Code Coverage"' + arguments: '--configuration $(BuildConfiguration) --settings coverage.runsettings --collect "XPlat Code Coverage" -- RunConfiguration.MaxCpuCount=0' - task: PublishCodeCoverageResults@2 displayName: Publish Code Coverage diff --git a/specs/001-migrate-itest-storage/checklists/requirements.md b/specs/001-migrate-itest-storage/checklists/requirements.md new file mode 100644 index 00000000..696ad654 --- /dev/null +++ b/specs/001-migrate-itest-storage/checklists/requirements.md @@ -0,0 +1,34 @@ +# Specification Quality Checklist: Parallelize SQL Integration Tests + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-03-27 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- Validation completed in one iteration; no unresolved checklist items. \ No newline at end of file diff --git a/specs/001-migrate-itest-storage/contracts/itest-parallelization.openapi.yaml b/specs/001-migrate-itest-storage/contracts/itest-parallelization.openapi.yaml new file mode 100644 index 00000000..f76f9345 --- /dev/null +++ b/specs/001-migrate-itest-storage/contracts/itest-parallelization.openapi.yaml @@ -0,0 +1,250 @@ +openapi: 3.0.3 +info: + title: Integration Test Migration and Parallel Validation Contract + version: 0.1.0 + description: >- + Contract for managing SQL-integration-test migration status, data-store profiles, + exception handling, and parallel execution validation evidence. +servers: + - url: https://ci.internal.dknet.local +paths: + /itest-projects: + get: + summary: List integration test projects and migration status + operationId: listIntegrationTestProjects + responses: + '200': + description: Current project migration state + content: + application/json: + schema: + type: object + properties: + projects: + type: array + items: + $ref: '#/components/schemas/IntegrationTestProject' + required: [projects] + /itest-projects/{projectId}/store-profile: + put: + summary: Set or update test store profile for a project + operationId: upsertTestStoreProfile + parameters: + - in: path + name: projectId + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TestStoreProfile' + responses: + '200': + description: Store profile updated + '400': + description: Invalid profile configuration + /parallel-runs: + post: + summary: Register and execute a parallel validation run + operationId: createParallelExecutionRun + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + commitSha: + type: string + targetProjectIds: + type: array + items: + type: string + parallelEnabled: + type: boolean + required: [commitSha, targetProjectIds, parallelEnabled] + responses: + '201': + description: Run accepted + content: + application/json: + schema: + $ref: '#/components/schemas/ParallelExecutionRun' + /parallel-runs/{runId}: + get: + summary: Fetch a single parallel execution run report + operationId: getParallelExecutionRun + parameters: + - in: path + name: runId + required: true + schema: + type: string + responses: + '200': + description: Run report + content: + application/json: + schema: + $ref: '#/components/schemas/ParallelExecutionRun' + '404': + description: Run not found + /migration-exceptions: + post: + summary: Record an approved migration exception + operationId: createMigrationException + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/MigrationExceptionRecord' + responses: + '201': + description: Exception recorded + '400': + description: Missing mitigation or approval data +components: + schemas: + IntegrationTestProject: + type: object + properties: + projectId: + type: string + projectPath: + type: string + moduleArea: + type: string + enum: [AspNet, EfCore] + currentStoreType: + type: string + enum: [SqlServerContainer, SqlServerConnection, SQLite, InMemory] + targetStoreType: + type: string + enum: [SQLite, InMemory] + requiresSqlContractLane: + type: boolean + migrationState: + type: string + enum: [Discovered, Assessed, Migrating, Validated, Exceptioned] + required: + - projectId + - projectPath + - moduleArea + - currentStoreType + - targetStoreType + - requiresSqlContractLane + - migrationState + TestStoreProfile: + type: object + properties: + profileId: + type: string + projectId: + type: string + provider: + type: string + enum: [SQLite, InMemory, SqlServerContainer] + isolationMode: + type: string + enum: [PerTest, PerClass, PerRun] + databaseNameStrategy: + type: string + enum: [GuidPerTest, TimestampedPerClass, Fixed] + setupStrategy: + type: string + enum: [Fixture, Factory, Inline] + teardownStrategy: + type: string + enum: [DisposeContext, DropDatabase, ContainerStop] + parallelSafe: + type: boolean + required: + - profileId + - projectId + - provider + - isolationMode + - databaseNameStrategy + - setupStrategy + - teardownStrategy + - parallelSafe + ParallelExecutionRun: + type: object + properties: + runId: + type: string + commitSha: + type: string + startedAtUtc: + type: string + format: date-time + endedAtUtc: + type: string + format: date-time + targetProjects: + type: array + items: + type: string + parallelEnabled: + type: boolean + passCount: + type: integer + minimum: 0 + failCount: + type: integer + minimum: 0 + contentionFailureCount: + type: integer + minimum: 0 + durationSeconds: + type: integer + minimum: 0 + required: + - runId + - commitSha + - startedAtUtc + - endedAtUtc + - targetProjects + - parallelEnabled + - passCount + - failCount + - contentionFailureCount + - durationSeconds + MigrationExceptionRecord: + type: object + properties: + exceptionId: + type: string + projectId: + type: string + reasonCode: + type: string + enum: [ProviderSpecificSql, TransactionSemantics, UnsupportedTranslation, Other] + description: + type: string + approvedBy: + type: string + reviewByDate: + type: string + format: date + mitigation: + type: string + required: + - exceptionId + - projectId + - reasonCode + - description + - approvedBy + - reviewByDate + - mitigation + example: + exceptionId: ex-efcore-relhelper-001 + projectId: src/EfCore/EfCore.Relational.Helpers.Tests + reasonCode: ProviderSpecificSql + description: Test validates SQL Server metadata behavior unavailable in SQLite. + approvedBy: module-owner + reviewByDate: 2026-06-25 + mitigation: Retain SQL contract-lane tests in targeted pipeline runs. \ No newline at end of file diff --git a/specs/001-migrate-itest-storage/data-model.md b/specs/001-migrate-itest-storage/data-model.md new file mode 100644 index 00000000..6ba4b701 --- /dev/null +++ b/specs/001-migrate-itest-storage/data-model.md @@ -0,0 +1,101 @@ +# Data Model: SQL Integration Test Parallelization Migration + +## Entity: IntegrationTestProject + +Represents a test project assessed and migrated under this feature. + +| Field | Type | Required | Description | +|---|---|---|---| +| `projectId` | string | Yes | Stable identifier (e.g., csproj-relative path key). | +| `projectPath` | string | Yes | Repository-relative project path. | +| `moduleArea` | enum | Yes | `AspNet` or `EfCore`. | +| `currentStoreType` | enum | Yes | `SqlServerContainer`, `SqlServerConnection`, `SQLite`, `InMemory`. | +| `targetStoreType` | enum | Yes | `SQLite` or `InMemory` for migrated lane. | +| `requiresSqlContractLane` | bool | Yes | Indicates whether provider-specific SQL behavior must be retained in separate lane. | +| `migrationState` | enum | Yes | `Discovered`, `Assessed`, `Migrating`, `Validated`, `Exceptioned`. | +| `owner` | string | No | Maintainer or module owner for the project. | + +### Validation Rules + +- `targetStoreType=InMemory` is only valid when tests do not depend on relational/provider semantics. +- `requiresSqlContractLane=true` requires at least one contract-lane test definition. +- `migrationState=Validated` requires successful parallel run evidence. + +## Entity: TestStoreProfile + +Defines how a project provisions and isolates its test database resources. + +| Field | Type | Required | Description | +|---|---|---|---| +| `profileId` | string | Yes | Unique profile identifier per project. | +| `projectId` | string | Yes | Foreign key to `IntegrationTestProject`. | +| `provider` | enum | Yes | `SQLite`, `InMemory`, `SqlServerContainer`. | +| `isolationMode` | enum | Yes | `PerTest`, `PerClass`, `PerRun`. | +| `databaseNameStrategy` | enum | Yes | `GuidPerTest`, `TimestampedPerClass`, `Fixed`. | +| `setupStrategy` | enum | Yes | `Fixture`, `Factory`, `Inline`. | +| `teardownStrategy` | enum | Yes | `DisposeContext`, `DropDatabase`, `ContainerStop`. | +| `parallelSafe` | bool | Yes | Indicates whether this profile is validated for parallel execution. | + +### Validation Rules + +- `parallelSafe=true` requires `databaseNameStrategy != Fixed` unless provider is pure in-memory per-test instance. +- `provider=SqlServerContainer` in migrated lane must use isolated DB naming if parallelized. +- `isolationMode=PerRun` is disallowed for migrated parallel-safe profiles. + +## Entity: ParallelExecutionRun + +Captures evidence that migrated tests are stable under parallel execution. + +| Field | Type | Required | Description | +|---|---|---|---| +| `runId` | string | Yes | Unique run identifier. | +| `commitSha` | string | Yes | Git commit tested. | +| `startedAtUtc` | datetime | Yes | Run start timestamp. | +| `endedAtUtc` | datetime | Yes | Run end timestamp. | +| `targetProjects` | string[] | Yes | Project IDs included in run. | +| `parallelEnabled` | bool | Yes | Must be true for this feature validation. | +| `passCount` | int | Yes | Passing tests count. | +| `failCount` | int | Yes | Failing tests count. | +| `contentionFailureCount` | int | Yes | Failures attributable to shared DB contention. | +| `durationSeconds` | int | Yes | End-to-end elapsed duration. | + +### Validation Rules + +- `parallelEnabled` must be true for runs used as migration acceptance evidence. +- `contentionFailureCount` must be 0 for qualifying acceptance runs. +- Exactly 10 consecutive qualifying runs are required for SC-003 validation. + +## Entity: MigrationExceptionRecord + +Tracks explicit exceptions where SQL-dependent tests remain on contract lane. + +| Field | Type | Required | Description | +|---|---|---|---| +| `exceptionId` | string | Yes | Unique exception identifier. | +| `projectId` | string | Yes | Foreign key to `IntegrationTestProject`. | +| `reasonCode` | enum | Yes | `ProviderSpecificSql`, `TransactionSemantics`, `UnsupportedTranslation`, `Other`. | +| `description` | string | Yes | Human-readable explanation. | +| `approvedBy` | string | Yes | Maintainer approval identity. | +| `reviewByDate` | date | Yes | Date to re-evaluate exception. | +| `mitigation` | string | Yes | How risk is covered in retained SQL lane. | + +### Validation Rules + +- Exception records require explicit mitigation and owner approval. +- `reviewByDate` must be within 90 days of creation. + +## Relationships + +- `IntegrationTestProject` 1:N `TestStoreProfile` +- `IntegrationTestProject` 1:N `MigrationExceptionRecord` +- `ParallelExecutionRun` N:M `IntegrationTestProject` + +## State Transitions + +`IntegrationTestProject.migrationState` transition rules: + +- `Discovered -> Assessed` after dependency classification is complete. +- `Assessed -> Migrating` after target store profile is approved. +- `Migrating -> Validated` after acceptance criteria (parallel stability + behavior equivalence) are met. +- `Assessed -> Exceptioned` when approved exception is recorded. +- `Migrating -> Exceptioned` if migration reveals non-portable SQL semantics requiring retained contract lane. \ No newline at end of file diff --git a/specs/001-migrate-itest-storage/plan.md b/specs/001-migrate-itest-storage/plan.md new file mode 100644 index 00000000..7dbfa83f --- /dev/null +++ b/specs/001-migrate-itest-storage/plan.md @@ -0,0 +1,89 @@ +# Implementation Plan: Parallelize SQL Integration Tests + +**Branch**: `001-migrate-itest-storage` | **Date**: 2026-03-27 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/001-migrate-itest-storage/spec.md` + +## Summary + +Migrate SQL-dependent integration test projects to parallel-safe execution by defaulting to isolated SQLite-based test stores and limiting InMemory usage to non-relational behavior tests only. Preserve a focused SQL Server Testcontainers contract lane for provider-specific semantics that cannot be represented faithfully in SQLite. Deliver deterministic isolation, stable parallel runs, and measurable CI time reduction. + +## Technical Context + +**Language/Version**: C# 13 / .NET 10+ +**Primary Dependencies**: xUnit, Shouldly, EF Core 10+, Microsoft.Data.Sqlite, Testcontainers.MsSql, ASP.NET Core test host (`WebApplicationFactory`) +**Storage**: SQLite in-memory/file-isolated test stores for migrated projects; SQL Server Testcontainers retained only for provider-specific contract coverage +**Testing**: `dotnet test` with xUnit collection and fixture isolation, integration verification via targeted project runs +**Target Platform**: DKNet CI (Azure Pipelines) and local developer macOS/Linux/Windows test execution +**Project Type**: .NET class-library/test-suite monorepo +**Performance Goals**: >=30% reduction in CI elapsed time for targeted integration test projects; zero SQL-contention failures across 10 consecutive runs +**Constraints**: Preserve constitution-mandated real-provider validation where SQL-specific behavior is required; avoid cross-test shared state; keep warning-free builds +**Scale/Scope**: 3 identified SQL-dependent test projects (AspNet Idempotency MsSqlStore tests, EF Specifications tests, EF Relational Helpers tests) plus CI parallelization configuration updates + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- [x] **Runtime baseline**: Targets remain .NET 10+ (`net10.0`); no older TFMs introduced. +- [x] **Zero warnings**: Plan includes warning-safe changes only; analyzers remain enforced. +- [x] **Nullability contracts**: Fixture and helper signatures remain nullable-safe and explicit. +- [x] **Documentation contract**: Any new public test helpers will include XML docs and headers when applicable. +- [x] **Test-first gate**: Plan sequences migration validation tests before implementation refactors. +- [x] **Real DB integration**: SQL Server Testcontainers lane is retained for provider-specific behaviors; InMemory is not used as a replacement for SQL semantics. +- [x] **Pattern integrity**: EF query/predicate conventions (including `.AsExpandable()` where relevant) are preserved in migrated tests. +- [x] **Security/secrets**: No secrets are committed; test configuration remains environment-driven. + +## Project Structure + +### Documentation (this feature) + +```text +specs/001-migrate-itest-storage/ +├── plan.md +├── research.md +├── data-model.md +├── quickstart.md +├── contracts/ +│ └── itest-parallelization.openapi.yaml +└── tasks.md +``` + +### Source Code (repository root) + +```text +src/ +├── AspNet/ +│ └── AspCore.Idempotency.MsSqlStore.Tests/ +│ ├── Fixtures/ +│ └── Integration/ +├── EfCore/ +│ ├── EfCore.Specifications.Tests/ +│ │ ├── Fixtures/ +│ │ └── *Tests.cs +│ └── EfCore.Relational.Helpers.Tests/ +│ ├── Fixtures/ +│ └── *Tests.cs +└── DKNet.FW.sln +``` + +**Structure Decision**: This feature is a test-infrastructure migration that touches existing integration-test projects in `src/AspNet` and `src/EfCore` and does not introduce new runtime modules. + +## Complexity Tracking + +No constitution violations requiring exception approval. + +## Post-Design Constitution Re-Check + +- [x] **Runtime baseline**: Design remains on .NET 10+ and does not alter TFM policy. +- [x] **Zero warnings**: Planned changes are confined to test fixtures/config and can remain analyzer-clean. +- [x] **Nullability contracts**: Data-store profiles and migration metadata include explicit required/optional fields. +- [x] **Documentation contract**: No production public API changes; test helper documentation requirements retained. +- [x] **Test-first + real infra**: Parallel migration validation includes retained SQL contract tests for provider-specific semantics. +- [x] **Pattern integrity**: No deviation from Specification/Repository/dynamic predicate rules. +- [x] **Security/secrets**: Contract model keeps credentials out of source and enforces config indirection. + +## Performance Comparison Criteria + +- Baseline window: capture at least 5 successful pre-migration CI runs for targeted projects. +- Post-migration window: capture at least 5 successful post-migration CI runs for targeted projects. +- Acceptance threshold: mean duration improvement must be >=30%. +- Stability gate: contention-related failures must remain 0 across 10 unchanged-commit runs. diff --git a/specs/001-migrate-itest-storage/quickstart.md b/specs/001-migrate-itest-storage/quickstart.md new file mode 100644 index 00000000..58df45ce --- /dev/null +++ b/specs/001-migrate-itest-storage/quickstart.md @@ -0,0 +1,94 @@ +# Quickstart: Parallelize SQL Integration Tests + +## Purpose + +Validate the migration strategy for SQL-dependent integration test projects so they can run in parallel without shared-state failures. + +## Prerequisites + +- .NET 10 SDK installed (see `src/global.json`). +- Docker available for retained SQL Server contract-lane tests. +- Repository cloned and feature branch checked out. + +## 1. Restore and Baseline Build + +Run from `src/`: + +```bash +dotnet restore DKNet.FW.sln +dotnet build DKNet.FW.sln --configuration Release +``` + +Expected outcome: +- Build succeeds with zero warnings. + +## 2. Run Targeted Projects in Parallel-Safe Mode + +Run targeted migrated projects: + +```bash +dotnet test AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj --configuration Release +dotnet test EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj --configuration Release +dotnet test EfCore/EfCore.Relational.Helpers.Tests/EfCore.Relational.Helpers.Tests.csproj --configuration Release +``` + +Expected outcome: +- Tests pass without shared SQL contention failures. + +## 3. Execute Full Solution Test Pass + +```bash +dotnet test DKNet.FW.sln --configuration Release --settings coverage.runsettings --collect "XPlat Code Coverage" +``` + +Expected outcome: +- Test suite completes successfully. +- Retained SQL contract-lane tests pass where provider-specific behavior is required. + +## 4. Stability Validation (10 Consecutive Runs) + +Execute the targeted migration test set 10 times on unchanged code. + +Example loop from `src/`: + +```bash +for i in {1..10}; do + echo "Run $i" + dotnet test AspNet/AspCore.Idempotency.MsSqlStore.Tests/AspCore.Idempotency.MsSqlStore.Tests.csproj --configuration Release -- RunConfiguration.MaxCpuCount=0 || break + dotnet test EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj --configuration Release -- RunConfiguration.MaxCpuCount=0 || break + dotnet test EfCore/EfCore.Relational.Helpers.Tests/EfCore.Relational.Helpers.Tests.csproj --configuration Release -- RunConfiguration.MaxCpuCount=0 || break +done +``` + +Expected outcome: +- 0 failures attributed to shared database contention. +- Deterministic pass/fail behavior across runs. + +Evidence capture template: + +| Run | Result | Contention Failures | Duration (s) | +|---|---|---|---| +| 1 | PASS/FAIL | 0 | n/a | +| 2 | PASS/FAIL | 0 | n/a | +| ... | ... | ... | ... | +| 10 | PASS/FAIL | 0 | n/a | + +## 5. CI Throughput Comparison + +Compare elapsed time for targeted integration test jobs before and after migration. + +Expected outcome: +- >=30% reduction in end-to-end CI feedback time for targeted projects. + +Comparison formula: + +- Baseline average duration: mean of at least 5 pre-migration runs. +- Post-migration average duration: mean of at least 5 post-migration runs. +- Improvement % = `((Baseline - Post) / Baseline) * 100`. + +## 6. Exception Recording + +For any scenario retained on SQL contract lane: + +- Record a migration exception with reason, mitigation, approver, and review date. +- Ensure retained coverage remains active in CI. \ No newline at end of file diff --git a/specs/001-migrate-itest-storage/research.md b/specs/001-migrate-itest-storage/research.md new file mode 100644 index 00000000..8ea855a3 --- /dev/null +++ b/specs/001-migrate-itest-storage/research.md @@ -0,0 +1,81 @@ +# Research: Parallelize SQL Integration Tests + +**Phase**: 0 - Research and decision consolidation +**Date**: 2026-03-27 + +## 1. Migration Store Strategy Per Test Type + +**Decision**: Use isolated SQLite for migrated integration tests by default, and restrict `UseInMemoryDatabase` to non-relational behavior tests only. + +**Rationale**: SQLite preserves more relational behavior (constraints, transactions, SQL translation differences) than EF InMemory while remaining lightweight and parallel-friendly. + +**Alternatives considered**: +- Full EF InMemory migration for all projects: rejected because it can mask SQL translation and relational constraint behaviors. +- Keep all tests on shared SQL Server container: rejected because shared infrastructure blocks safe parallelization and increases flakiness. + +## 2. Constitution Compliance With Real-Provider Validation + +**Decision**: Keep a focused SQL Server Testcontainers contract lane for provider-specific behaviors that SQLite cannot represent. + +**Rationale**: Repository constitution requires real-provider validation for EF Core integration behavior that depends on provider semantics; preserving a smaller SQL lane satisfies this while enabling broad parallel migration. + +**Alternatives considered**: +- Remove all SQL Server integration tests: rejected due to constitutional non-compliance and risk of provider drift. +- Keep all existing SQL tests unchanged: rejected due to inability to achieve requested parallel execution gains. + +## 3. Test Isolation Model + +**Decision**: Enforce per-test or per-test-class unique database identities and deterministic setup/teardown for all migrated projects. + +**Rationale**: Parallel safety requires strict data isolation. Shared database names or long-lived contexts create non-deterministic cross-test contamination. + +**Alternatives considered**: +- Single shared SQLite database per project: rejected due to data race and locking risk. +- Serialized test execution: rejected because it does not meet parallelization goal. + +## 4. Initial Project Scope + +**Decision**: Target these SQL-dependent integration test projects first: +- `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests` +- `src/EfCore/EfCore.Specifications.Tests` +- `src/EfCore/EfCore.Relational.Helpers.Tests` + +**Rationale**: These projects currently instantiate SQL Server via `Testcontainers.MsSql` and/or `UseSqlServer` in fixtures/tests and are the highest-impact candidates for parallel-safe migration. + +**Alternatives considered**: +- Repo-wide test migration in one sweep: rejected due to larger blast radius and harder verification. +- Single-project pilot only: rejected as insufficient to satisfy "all SQL itest projects" scope. + +## 5. Parallel Execution Enablement + +**Decision**: Enable parallel execution at test runner level while retaining collection boundaries only where shared fixtures are unavoidable. + +**Rationale**: Over-broad collection usage can inadvertently serialize tests. Parallel-friendly fixture design should replace shared-fixture serialization where practical. + +**Alternatives considered**: +- Keep current collection design unchanged: rejected because it may continue to suppress parallelism. +- Remove all collection controls indiscriminately: rejected because a few contract-lane tests may still require controlled fixture scope. + +## 6. CI Validation Strategy + +**Decision**: Validate migration success using repeated parallel runs (10 consecutive unchanged-commit runs) and track contention/flakiness metrics. + +**Rationale**: One successful run is not enough to prove isolation stability. Repetition validates determinism and contention removal. + +**Alternatives considered**: +- Single run validation: rejected due to weak confidence in parallel stability. +- Time-only metric without reliability checks: rejected because speed gains without stability are not acceptable. + +## 7. Migration Exception Governance (Implemented) + +Approved exception criteria for retaining SQL contract-lane coverage: + +- SQL-provider query translation differs from SQLite in a way that changes behavior under test. +- Transaction semantics (locking/isolation) are part of the behavior under test. +- Relational helper behavior directly depends on SQL Server metadata or SQL dialect. + +Required exception metadata: + +- Reason code (`ProviderSpecificSql`, `TransactionSemantics`, `UnsupportedTranslation`, `Other`). +- Mitigation and retained test coverage reference. +- Approver and review-by date (within 90 days). \ No newline at end of file diff --git a/specs/001-migrate-itest-storage/spec.md b/specs/001-migrate-itest-storage/spec.md new file mode 100644 index 00000000..2cc508ea --- /dev/null +++ b/specs/001-migrate-itest-storage/spec.md @@ -0,0 +1,105 @@ +# Feature Specification: Parallelize SQL Integration Tests + +**Feature Branch**: `001-migrate-itest-storage` +**Created**: 2026-03-27 +**Status**: Draft +**Input**: User description: "migrate all itest project that using sql to in-memory or sqlite to enable the parallel execution" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Migrate SQL-Dependent Test Projects (Priority: P1) + +As a maintainer, I can run integration test projects that previously required shared SQL infrastructure by using isolated in-memory or file-less local database options so tests can run safely in parallel. + +**Why this priority**: This is the core scope of the request and unlocks immediate parallel execution without external database contention. + +**Independent Test**: Select any one SQL-dependent integration test project, run it in parallel mode with other test classes, and confirm tests pass without requiring a shared SQL instance. + +**Acceptance Scenarios**: + +1. **Given** a test project currently relying on a shared SQL database, **When** it is migrated to an isolated in-memory or local SQLite-backed test setup, **Then** the project executes successfully without shared database dependencies. +2. **Given** multiple tests in the same migrated project start concurrently, **When** they run with isolated data stores, **Then** they complete without cross-test data contamination. + +--- + +### User Story 2 - Preserve Test Intent and Reliability (Priority: P2) + +As a maintainer, I can trust that migrated test projects still validate the same behaviors and business rules as before migration. + +**Why this priority**: Parallel speed improvements are only valuable if behavioral coverage remains accurate and stable. + +**Independent Test**: Compare pre-migration and post-migration outcomes for representative scenarios in each affected project and verify expected pass/fail behavior remains equivalent. + +**Acceptance Scenarios**: + +1. **Given** a migrated integration test case with defined expected outcome, **When** it runs on the new test store option, **Then** it produces the same business-level result as before migration. +2. **Given** repeated executions of migrated tests, **When** the suite is run multiple times, **Then** it yields consistent outcomes without intermittent failures caused by shared state. + +--- + +### User Story 3 - Enable Faster Parallel CI Feedback (Priority: P3) + +As a contributor, I can execute the full integration-test set in parallel so pull request feedback arrives faster and with fewer infrastructure-related failures. + +**Why this priority**: This delivers the user-facing productivity outcome of the migration effort. + +**Independent Test**: Run the full impacted integration test set in parallel mode in CI and confirm completion time and stability targets are met. + +**Acceptance Scenarios**: + +1. **Given** all targeted projects have been migrated, **When** CI executes integration tests with parallelization enabled, **Then** the run completes successfully without database lock/contention failures. +2. **Given** the same commit is tested repeatedly, **When** parallel CI test runs are executed, **Then** failure rates attributable to shared SQL infrastructure are eliminated. + +### Edge Cases + +- What happens when a test depends on SQL-specific behavior not represented by an in-memory provider? +- How does the system handle tests that assume transactional behavior across multiple operations when running on SQLite? +- What happens if two parallel tests accidentally reuse the same logical database identifier? +- How does the suite handle projects that cannot be migrated fully and still require a SQL-backed path? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: The test suite MUST identify all integration test projects currently dependent on shared SQL infrastructure and classify them as in scope, out of scope, or partially migratable. +- **FR-002**: Each in-scope SQL-dependent integration test project MUST be migrated to run with an isolated non-shared test data store option (in-memory or SQLite) suitable for parallel execution. +- **FR-003**: Migrated projects MUST support concurrent test execution without cross-test data leakage. +- **FR-004**: Migrated tests MUST preserve existing business-behavior assertions and expected outcomes. +- **FR-005**: Test setup and teardown behavior MUST guarantee deterministic state reset between tests. +- **FR-006**: The integration test pipeline MUST execute targeted projects with parallelization enabled. +- **FR-007**: The migration MUST include documented handling for scenarios that cannot be represented faithfully with the selected non-shared data store. +- **FR-008**: The feature MUST define rollback criteria for any migrated project that shows behavior drift after migration. + +### Constitution Alignment *(mandatory)* + +- **CA-001 Runtime Baseline**: The feature MUST remain compatible with .NET 10+ and `net10.0` targets. +- **CA-002 Build Quality**: The feature MUST preserve zero-warning builds with analyzers enabled. +- **CA-003 Nullability**: The feature MUST define nullable/validation behavior for any new test inputs and outputs. +- **CA-004 Documentation**: Publicly consumable test fixtures/helpers introduced by the migration MUST include XML docs and required file headers. +- **CA-005 Testing**: Behavior changes MUST define or update tests first; where true database-provider behavior remains under test, coverage MUST continue using real-provider integration testing. +- **CA-006 Patterns**: If dynamic predicates are involved in migrated tests, the feature MUST preserve Specification/Repository composition requirements including expression expansion behavior. +- **CA-007 Security**: The feature MUST document boundary validation and safe handling of configuration and connection data used in tests. + +### Assumptions & Dependencies + +- Existing integration tests already encode expected business behavior, allowing migration verification by outcome equivalence. +- Parallel execution settings are available in local and CI test runners. +- Some SQL-provider-specific behavior may remain in dedicated non-parallel coverage where equivalence is not feasible. +- Migration scope is limited to integration test projects currently relying on SQL stores; unrelated test projects are out of scope. + +### Key Entities *(include if feature involves data)* + +- **Integration Test Project**: A test project containing end-to-end or repository-level tests, including metadata for current data-store dependency and migration status. +- **Test Store Profile**: The per-test-project configuration that defines whether tests run against in-memory, SQLite, or SQL-backed storage. +- **Parallel Execution Run**: A full test execution event with concurrency enabled, used to validate isolation, determinism, and stability. +- **Migration Exception Record**: Documentation artifact for scenarios intentionally retained on SQL due to provider-specific behavior requirements. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of in-scope SQL-dependent integration test projects are migrated to isolated in-memory or SQLite execution modes. +- **SC-002**: 100% of migrated projects can run with parallel execution enabled and complete without shared-state contamination failures. +- **SC-003**: Across 10 consecutive CI runs on unchanged code, migrated test projects show 0 failures attributed to shared SQL infrastructure contention. +- **SC-004**: End-to-end integration test feedback time in CI for targeted projects improves by at least 30% compared with the established pre-migration baseline. +- **SC-005**: At least 95% of previously passing integration tests in migrated projects continue to pass with equivalent business outcomes, with remaining deltas explicitly documented as migration exceptions. diff --git a/specs/001-migrate-itest-storage/tasks.md b/specs/001-migrate-itest-storage/tasks.md new file mode 100644 index 00000000..e0124232 --- /dev/null +++ b/specs/001-migrate-itest-storage/tasks.md @@ -0,0 +1,190 @@ +# Tasks: Parallelize SQL Integration Tests + +**Input**: Design documents from `/specs/001-migrate-itest-storage/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/itest-parallelization.openapi.yaml, quickstart.md + +**Tests**: Tests are REQUIRED for behavior changes. Define failing tests before implementation tasks (red-green-refactor). + +**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently. + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Establish migration inventory, baseline metrics, and run configuration for parallelization work. + +- [ ] T001 Create migration inventory document for SQL-dependent integration projects in `specs/001-migrate-itest-storage/research.md` +- [ ] T002 Capture pre-migration baseline timing and failure data for targeted projects in `specs/001-migrate-itest-storage/quickstart.md` +- [ ] T003 [P] Add test-run notes for repeated stability validation in `specs/001-migrate-itest-storage/quickstart.md` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Build shared test-store profile and isolation conventions required by all stories. + +**CRITICAL**: No user story implementation starts before this phase completes. + +- [ ] T004 Define store profile matrix (SQLite/InMemory/SQL contract lane) for each targeted project in `specs/001-migrate-itest-storage/data-model.md` +- [ ] T005 Define migration exception criteria and approval flow in `specs/001-migrate-itest-storage/data-model.md` +- [ ] T006 [P] Align contract operations and payload fields with migration workflow in `specs/001-migrate-itest-storage/contracts/itest-parallelization.openapi.yaml` +- [ ] T007 [P] Add shared guidance for unique database naming and teardown guarantees in `specs/001-migrate-itest-storage/quickstart.md` +- [ ] T008 Record constitution-safe rule for retaining SQL provider-specific tests in `specs/001-migrate-itest-storage/research.md` + +**Checkpoint**: Foundation is complete and user stories can proceed. + +--- + +## Phase 3: User Story 1 - Migrate SQL-Dependent Test Projects (Priority: P1) 🎯 MVP + +**Goal**: Convert SQL-dependent integration tests to parallel-safe isolated stores (SQLite first, InMemory only where relational behavior is not required). + +**Independent Test**: Run each migrated project with parallel execution and confirm no shared-state contamination. + +### Tests for User Story 1 (REQUIRED) + +- [ ] T009 [P] [US1] Add failing isolation tests for fixture database uniqueness in `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs` +- [ ] T010 [P] [US1] Add failing fixture isolation tests for specifications project in `src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs` +- [ ] T011 [P] [US1] Add failing parallel isolation tests for relational helpers project in `src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs` + +### Implementation for User Story 1 + +- [x] T012 [US1] Refactor SQL container fixture to isolated SQL contract-lane profile in `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Fixtures/ApiFixture.cs` +- [x] T013 [US1] Update idempotency integration tests to use migrated fixture lifecycle in `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs` +- [x] T014 [US1] Refactor specifications fixture from shared SQL container to isolated profile in `src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs` +- [x] T015 [US1] Update specifications setup assertions for new provider profile in `src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs` +- [x] T016 [US1] Refactor relational helpers fixture to isolated profile in `src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs` +- [x] T017 [US1] Update relational helper tests for migrated fixture behavior in `src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelperTests.cs` +- [x] T018 [US1] Update advanced helper tests for per-test database identity in `src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs` +- [ ] T019 [US1] Add explicit per-test teardown and disposal assertions across migrated fixtures in `src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs` + +**Checkpoint**: SQL-dependent integration projects run with isolated parallel-safe stores. + +--- + +## Phase 4: User Story 2 - Preserve Test Intent and Reliability (Priority: P2) + +**Goal**: Ensure migrated tests still verify equivalent business behavior and preserve provider-specific contract coverage. + +**Independent Test**: Execute representative migrated scenarios and retained SQL contract-lane scenarios; compare outcomes to baseline. + +### Tests for User Story 2 (REQUIRED) + +- [ ] T020 [P] [US2] Add failing equivalence assertions for idempotency result behavior in `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs` +- [ ] T021 [P] [US2] Add failing equivalence assertions for specifications query behavior in `src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs` +- [ ] T022 [P] [US2] Add failing provider-specific contract tests retained for SQL lane in `src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs` + +### Implementation for User Story 2 + +- [ ] T023 [US2] Implement behavior-equivalence verification helpers for idempotency tests in `src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs` +- [ ] T024 [US2] Implement behavior-equivalence verification helpers for specifications tests in `src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs` +- [x] T025 [US2] Implement retained SQL contract-lane fixture path for provider-specific tests in `src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs` +- [x] T026 [US2] Document migration exception cases and mitigation in `specs/001-migrate-itest-storage/research.md` +- [x] T027 [US2] Align migration exception schema examples with implemented contract-lane rules in `specs/001-migrate-itest-storage/contracts/itest-parallelization.openapi.yaml` + +**Checkpoint**: Reliability and behavioral equivalence are demonstrated with explicit exception handling. + +--- + +## Phase 5: User Story 3 - Enable Faster Parallel CI Feedback (Priority: P3) + +**Goal**: Enable stable parallel execution in CI and produce measurable cycle-time improvements. + +**Independent Test**: Run targeted projects in parallel mode repeatedly in CI-like execution and confirm throughput and stability metrics. + +### Tests for User Story 3 (REQUIRED) + +- [ ] T028 [P] [US3] Add failing CI-verification test notes for repeated parallel execution in `specs/001-migrate-itest-storage/quickstart.md` +- [ ] T029 [P] [US3] Add failing acceptance checks for contention-free repeated runs in `specs/001-migrate-itest-storage/plan.md` + +### Implementation for User Story 3 + +- [x] T030 [US3] Update CI pipeline test command strategy for targeted parallel projects in `azure-pipelines.yml` +- [x] T031 [US3] Update PR pipeline parallel test execution for targeted projects in `ai-pr-review.azure-pipelines.yml` +- [x] T032 [US3] Add 10-run stability validation procedure and evidence capture template in `specs/001-migrate-itest-storage/quickstart.md` +- [x] T033 [US3] Record post-migration performance comparison criteria and thresholds in `specs/001-migrate-itest-storage/plan.md` + +**Checkpoint**: Parallel CI execution is enabled and measurable improvement tracking is in place. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Final validation, documentation consistency, and quality gates. + +- [ ] T034 [P] Reconcile spec, plan, research, and quickstart wording for final terminology consistency in `specs/001-migrate-itest-storage/spec.md` +- [ ] T035 [P] Run full quickstart command sequence and record final outcomes in `specs/001-migrate-itest-storage/quickstart.md` +- [ ] T036 Run `dotnet build DKNet.FW.sln --configuration Release` and log zero-warning confirmation in `specs/001-migrate-itest-storage/quickstart.md` +- [ ] T037 Run `dotnet test DKNet.FW.sln --configuration Release --settings coverage.runsettings --collect "XPlat Code Coverage"` and log evidence in `specs/001-migrate-itest-storage/quickstart.md` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- Setup (Phase 1): no dependencies. +- Foundational (Phase 2): depends on Setup completion; blocks all user stories. +- User Story phases (Phase 3 to Phase 5): depend on Foundational completion. +- Polish (Phase 6): depends on completion of desired user stories. + +### User Story Dependencies + +- US1 (P1): starts after Phase 2; no dependency on other stories. +- US2 (P2): starts after Phase 2 and should validate behavior from US1 outputs. +- US3 (P3): starts after Phase 2 but practically depends on US1 and US2 migration outputs for CI proof. + +### Within Each User Story + +- Write tests first and confirm they fail. +- Implement fixture/store-profile changes. +- Update test logic and assertions. +- Validate story independently before moving forward. + +### Parallel Opportunities + +- Setup tasks marked [P] can run in parallel. +- Foundational tasks marked [P] can run in parallel. +- In US1, T009 to T011 can run in parallel. +- In US2, T020 to T022 can run in parallel. +- In US3, T028 and T029 can run in parallel. +- Polish tasks marked [P] can run in parallel. + +--- + +## Parallel Example: User Story 1 + +```bash +# Parallel test creation for US1 +Task T009: Isolation tests in AspCore.Idempotency.MsSqlStore.Tests +Task T010: Isolation tests in EfCore.Specifications.Tests +Task T011: Isolation tests in EfCore.Relational.Helpers.Tests + +# Parallel fixture migration after tests are in place +Task T012: Migrate ApiFixture +Task T014: Migrate TestDbFixture +Task T016: Migrate SqlServerFixture +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1) + +1. Complete Phase 1 and Phase 2. +2. Complete US1 tasks (T009 to T019). +3. Validate targeted migrated projects in parallel-safe mode. +4. Stop and confirm MVP value before expanding scope. + +### Incremental Delivery + +1. Deliver US1 migration for core project set. +2. Deliver US2 behavioral equivalence and exception governance. +3. Deliver US3 CI throughput and reliability optimization. +4. Complete polish and final evidence capture. + +### Parallel Team Strategy + +1. Team aligns on foundational profile rules. +2. Developer A: AspNet test migration tasks. +3. Developer B: EfCore specifications migration tasks. +4. Developer C: EfCore relational helpers and CI pipeline tasks. diff --git a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Fixtures/ApiFixture.cs b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Fixtures/ApiFixture.cs index 5c719327..a18befaa 100644 --- a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Fixtures/ApiFixture.cs +++ b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Fixtures/ApiFixture.cs @@ -21,6 +21,7 @@ public sealed class ApiFixture : WebApplicationFactory, IAsync { #region Fields + private readonly string _databaseName = $"Idem_{Guid.NewGuid():N}"; private MsSqlContainer? _container; #endregion @@ -32,6 +33,8 @@ public sealed class ApiFixture : WebApplicationFactory, IAsync /// public string ConnectionString { get; private set; } = string.Empty; + internal string DatabaseName => _databaseName; + /// /// Gets the HTTP client for making requests to the test application. /// @@ -90,12 +93,13 @@ public async Task InitializeAsync() { // Create and start SQL Server container _container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest") - .WithPassword("DKNetTest@123!") + .WithPassword($"A{Guid.NewGuid():N}a!") .WithCleanUp(true) .Build(); await _container.StartAsync(); - ConnectionString = _container.GetConnectionString(); + ConnectionString = _container.GetConnectionString() + .Replace("Database=master", $"Database={_databaseName}", StringComparison.OrdinalIgnoreCase); // Create the HTTP client HttpClient ??= CreateClient(); diff --git a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs index 711ec070..e60a346d 100644 --- a/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs +++ b/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests/Integration/IdempotencyIntegrationTests.cs @@ -5,7 +5,6 @@ using System.Net; using System.Net.Http.Json; -using System.Text.Json; using AspCore.Idempotency.ApiTests; using AspCore.Idempotency.MsSqlStore.Tests.Fixtures; @@ -32,6 +31,21 @@ public async Task ApiHealthCheck() content.ShouldNotBeNull(); } + [Fact] + public async Task ApiFixture_UsesIsolatedDatabaseConnectionString() + { + // Arrange + await using var dbContext = fixture.GetDbContext(); + + // Act + var connectionString = dbContext.Database.GetConnectionString(); + + // Assert + connectionString.ShouldNotBeNullOrWhiteSpace(); + connectionString.ShouldContain($"Database={fixture.DatabaseName}"); + connectionString.ShouldNotContain("Database=master"); + } + [Fact] public async Task CreateItem_ConcurrentRequestsWithSameKey_OnlyOneProcessed() { @@ -245,8 +259,7 @@ public async Task CreateItem_WithSameIdempotencyKey_SecondRequest_ReturnsCachedR Headers = { { "X-Idempotency-Key", idempotencyKey } }, Content = JsonContent.Create(request) }; - var response1 = await fixture.HttpClient!.SendAsync(httpRequest1); - var item1 = await response1.Content.ReadAsStringAsync(); + await fixture.HttpClient!.SendAsync(httpRequest1); // Act - Second request with same idempotency key var httpRequest2 = new HttpRequestMessage(HttpMethod.Post, "/api/items") diff --git a/src/DKNet.FW.sln.DotSettings.user b/src/DKNet.FW.sln.DotSettings.user index f443ceb1..9c06e63b 100644 --- a/src/DKNet.FW.sln.DotSettings.user +++ b/src/DKNet.FW.sln.DotSettings.user @@ -585,25 +585,12 @@ </AssemblyExplorer> True True - <SessionState ContinuousTestingMode="0" Name="All tests from &lt;AspNet&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> + <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;AspNet&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> <Solution /> </SessionState> - <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from &lt;AspNet&gt;\&lt;AspCore.Idempotency.Tests&gt;" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Or> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src/AspNet/AspCore.Idempotency.Tests" Presentation="&lt;AspNet&gt;\&lt;AspCore.Idempotency.Tests&gt;" /> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src/AspNet/AspCore.Idempotency.MsSqlStore.Tests" Presentation="&lt;AspNet&gt;\&lt;AspCore.Idempotency.MsSqlStore.Tests&gt;" /> - </Or> -</SessionState> - <SessionState ContinuousTestingMode="0" Name="Session" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> - <Or> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;AspNet&gt;" /> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;Core&gt;" /> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;EfCore&gt;" /> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;Services&gt;" /> - <Project Location="/Users/steven/_CODE/DRUNK/DKNet/src" Presentation="&lt;SlimBus&gt;" /> - </Or> -</SessionState> + + diff --git a/src/EfCore/DKNet.EfCore.Extensions/Configurations/IDataSeedingConfiguration.cs b/src/EfCore/DKNet.EfCore.Extensions/Configurations/IDataSeedingConfiguration.cs index 05a5543b..bab3bf5a 100644 --- a/src/EfCore/DKNet.EfCore.Extensions/Configurations/IDataSeedingConfiguration.cs +++ b/src/EfCore/DKNet.EfCore.Extensions/Configurations/IDataSeedingConfiguration.cs @@ -14,6 +14,12 @@ public interface IDataSeedingConfiguration { #region Properties + /// + /// The order in which this seeding configuration should be applied relative to other configurations. Configurations + /// with + /// + int Order { get; } + /// /// Optional asynchronous seeding callback. The function receives the current , a boolean /// indicating whether the seeding call should run as part of migrations/initialization, a @@ -40,7 +46,7 @@ public interface IDataSeedingConfiguration /// /// Generic base class for data seeding configurations. Implementers can provide model-managed seed data via -/// or an asynchronous seed routine via . +/// or an asynchronous seed routine via . /// /// The entity type to seed. public abstract class DataSeedingConfiguration : IDataSeedingConfiguration where TEntity : class @@ -50,21 +56,25 @@ public abstract class DataSeedingConfiguration : IDataSeedingConfigurat /// public Type EntityType => typeof(TEntity); - /// - /// Strongly typed collection of seed data for the entity. Implementations may populate this collection with - /// instances of to be used as model-managed seed data. - /// - protected virtual ICollection HasData { get; } = []; + /// + public IEnumerable HasData => GetData(); + + /// + public virtual int Order => 0; + /// - IEnumerable IDataSeedingConfiguration.HasData => HasData; + public virtual Func? SeedAsync => null; + + #endregion + + #region Methods /// - /// Optional asynchronous seed callback. By defaults this is null which means no runtime seeding action is - /// provided. - /// Override to supply an async seeding routine. + /// Gets the collection of seed data for the target entity type./> /// - public virtual Func? SeedAsync => null; + /// + protected abstract ICollection GetData(); #endregion } \ No newline at end of file diff --git a/src/EfCore/DKNet.EfCore.Extensions/Extensions/EfCoreDataSeedingExtensions.cs b/src/EfCore/DKNet.EfCore.Extensions/Extensions/EfCoreDataSeedingExtensions.cs index ccc64042..197a0344 100644 --- a/src/EfCore/DKNet.EfCore.Extensions/Extensions/EfCoreDataSeedingExtensions.cs +++ b/src/EfCore/DKNet.EfCore.Extensions/Extensions/EfCoreDataSeedingExtensions.cs @@ -46,14 +46,17 @@ internal static void RegisterDataSeeding(this ModelBuilder modelBuilder, params ArgumentNullException.ThrowIfNull(modelBuilder); var seedingTypes = assemblies.GetDataSeedingTypes(); - foreach (var seedingType in seedingTypes) - { - if (Activator.CreateInstance(seedingType) is not IDataSeedingConfiguration seedingInstance) continue; + var instances = seedingTypes + .Select(t => Activator.CreateInstance(t) as IDataSeedingConfiguration) + .OfType() + .OrderBy(s => s.Order); - var data = seedingInstance.HasData?.ToList() ?? []; + foreach (var item in instances) + { + var data = item.HasData?.ToList() ?? []; if (data.Count == 0) continue; - var entityType = seedingInstance.EntityType; + var entityType = item.EntityType; // ModelBuilder.Entity(Type).HasData accepts params object[] modelBuilder.Entity(entityType).HasData(data.ToArray()); } diff --git a/src/EfCore/EfCore.Extensions.Tests/DataSeedingTests.cs b/src/EfCore/EfCore.Extensions.Tests/DataSeedingTests.cs index 131b9689..fe519e59 100644 --- a/src/EfCore/EfCore.Extensions.Tests/DataSeedingTests.cs +++ b/src/EfCore/EfCore.Extensions.Tests/DataSeedingTests.cs @@ -6,11 +6,12 @@ namespace EfCore.Extensions.Tests; // Test seeding configuration for testing public class UserSeedingConfiguration : DataSeedingConfiguration { - #region Properties + #region Methods - protected override ICollection HasData => + protected override ICollection GetData() => [ - new(1, "seeded1") + new( + 1, "seeded1") { FirstName = "Seeded", LastName = "User1" }, diff --git a/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelperTests.cs b/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelperTests.cs index 659a6183..9ef71ab2 100644 --- a/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelperTests.cs +++ b/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelperTests.cs @@ -22,6 +22,17 @@ public async Task CheckTableExistsFailed() await action.ShouldThrowAsync(); } + [Fact] + public async Task ConnectionString_ShouldUseIsolatedDatabaseName() + { + await fixture.EnsureSqlReadyAsync(); + + var connectionString = fixture.GetConnectionString(); + + connectionString.ShouldContain("Database=TestDb_"); + connectionString.ShouldNotContain("Database=master"); + } + [Fact] public async Task CreateTable() { diff --git a/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs b/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs index c9922563..115a2dc4 100644 --- a/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs +++ b/src/EfCore/EfCore.Relational.Helpers.Tests/DbContextHelpersAdvancedTests.cs @@ -50,9 +50,8 @@ public async Task CreateTableAsync_WithNonExistingDatabase_ShouldCreateDatabaseA // Arrange await fixture.EnsureSqlReadyAsync(); - // Create a unique database name to ensure it doesn't exist - var uniqueConnectionString = fixture.GetConnectionString() - .Replace("master", $"TestDb_{Guid.NewGuid():N}", StringComparison.OrdinalIgnoreCase); + // Create a unique database name to ensure it doesn't exist. + var uniqueConnectionString = fixture.CreateIsolatedConnectionString(); await using var db = new TestDbContext( new DbContextOptionsBuilder() diff --git a/src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs b/src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs index 626b740a..0bccfb90 100644 --- a/src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs +++ b/src/EfCore/EfCore.Relational.Helpers.Tests/Fixtures/SqlServerFixture.cs @@ -7,13 +7,20 @@ public class SqlServerFixture : IAsyncLifetime { #region Fields + private readonly string _databaseName = $"TestDb_{Guid.NewGuid():N}"; private MsSqlContainer? _container; #endregion #region Methods - public Task DisposeAsync() => Task.CompletedTask; + public async Task DisposeAsync() + { + if (_container == null) return; + + await _container.StopAsync(); + await _container.DisposeAsync(); + } public async Task EnsureSqlReadyAsync() { @@ -26,13 +33,18 @@ public async Task EnsureSqlReadyAsync() public string GetConnectionString() => _container?.GetConnectionString() - .Replace("Database=master", "Database=TestDb", StringComparison.OrdinalIgnoreCase) ?? + .Replace("Database=master", $"Database={_databaseName}", StringComparison.OrdinalIgnoreCase) ?? + throw new InvalidOperationException("SQL Server container is not initialized."); + + public string CreateIsolatedConnectionString() => + _container?.GetConnectionString() + .Replace("Database=master", $"Database=TestDb_{Guid.NewGuid():N}", StringComparison.OrdinalIgnoreCase) ?? throw new InvalidOperationException("SQL Server container is not initialized."); public async Task InitializeAsync() { _container = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest") - .WithPassword("a1ckZmGjwV8VqNdBUexV") + .WithPassword($"A{Guid.NewGuid():N}a!") //.WithReuse(true) .Build(); diff --git a/src/EfCore/EfCore.Specifications.Tests/DynamicPredicateBuilderExtensionsTests.cs b/src/EfCore/EfCore.Specifications.Tests/DynamicPredicateBuilderExtensionsTests.cs index 64bb4df6..36a88b1f 100644 --- a/src/EfCore/EfCore.Specifications.Tests/DynamicPredicateBuilderExtensionsTests.cs +++ b/src/EfCore/EfCore.Specifications.Tests/DynamicPredicateBuilderExtensionsTests.cs @@ -238,9 +238,11 @@ public void DynamicAnd_CaseInsensitivePropertyResolution_WorksCorrectly() // Assert var query = _context.Products.AsExpandable().Where(result); var sql = query.ToQueryString(); - sql.ShouldContain("[Name]"); - sql.ShouldContain("[Price]"); - sql.ShouldContain("[IsActive]"); + var normalizedSql = NormalizeSql(sql).ToLowerInvariant(); + normalizedSql.ShouldContain("where"); + normalizedSql.ShouldContain("p.name"); + normalizedSql.ShouldContain("p.price"); + normalizedSql.ShouldContain("p.isactive"); } [Fact] @@ -270,10 +272,18 @@ public void DynamicAnd_WithInvalidPropertyName_ReturnsUnchangedPredicate() // Assert: Should return original predicate unchanged var query = _context.Products.AsExpandable().Where(result); var sql = query.ToQueryString(); - sql.ShouldNotContain("NonExistent"); - sql.ShouldContain("[IsActive]"); + var normalizedSql = NormalizeSql(sql).ToLowerInvariant(); + normalizedSql.ShouldNotContain("nonexistent"); + normalizedSql.ShouldContain("where"); + normalizedSql.ShouldContain("p.isactive"); } + private static string NormalizeSql(string sql) + => sql + .Replace("\"", string.Empty, StringComparison.Ordinal) + .Replace("[", string.Empty, StringComparison.Ordinal) + .Replace("]", string.Empty, StringComparison.Ordinal); + [Fact] public void DynamicOr_WithInvalidPropertyName_ReturnsUnchangedPredicate() { diff --git a/src/EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj b/src/EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj index c9f365f7..d7acc79e 100644 --- a/src/EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj +++ b/src/EfCore/EfCore.Specifications.Tests/EfCore.Specifications.Tests.csproj @@ -19,9 +19,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + - diff --git a/src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs b/src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs index 191f0e08..1e02a68b 100644 --- a/src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs +++ b/src/EfCore/EfCore.Specifications.Tests/Fixtures/TestDbFixture.cs @@ -1,5 +1,5 @@ using Bogus; -using Testcontainers.MsSql; +using Microsoft.Data.Sqlite; namespace EfCore.Specifications.Tests.Fixtures; @@ -10,7 +10,7 @@ public class TestDbFixture : IAsyncLifetime private readonly Faker _categoryFaker; private readonly Faker _orderFaker; private readonly Faker _productFaker; - private MsSqlContainer? _msSqlContainer; + private SqliteConnection? _sqliteConnection; #endregion @@ -52,22 +52,18 @@ public async Task DisposeAsync() { if (Db != null) await Db.DisposeAsync(); - if (_msSqlContainer != null) await _msSqlContainer.DisposeAsync(); + if (_sqliteConnection != null) await _sqliteConnection.DisposeAsync(); } public async Task InitializeAsync() { - // Start SQL Server container - _msSqlContainer = new MsSqlBuilder("mcr.microsoft.com/mssql/server:2022-latest") - .WithPassword("YourStrong@Passw0rd") - .WithCleanUp(true) - .Build(); + // Keep the shared connection open so SQLite in-memory state survives for the fixture lifetime. + _sqliteConnection = new SqliteConnection($"Data Source=file:specs-{Guid.NewGuid():N}?mode=memory&cache=shared"); + await _sqliteConnection.OpenAsync(); - await _msSqlContainer.StartAsync(); - - // Create DbContext with SQL Server connection + // Create DbContext with SQLite connection var options = new DbContextOptionsBuilder() - .UseSqlServer(_msSqlContainer.GetConnectionString()) + .UseSqlite(_sqliteConnection) .EnableSensitiveDataLogging() .EnableDetailedErrors() .LogTo(Console.WriteLine, LogLevel.Information) diff --git a/src/EfCore/EfCore.Specifications.Tests/RepositorySpecTests.cs b/src/EfCore/EfCore.Specifications.Tests/RepositorySpecTests.cs index e049e8d0..be4d111c 100644 --- a/src/EfCore/EfCore.Specifications.Tests/RepositorySpecTests.cs +++ b/src/EfCore/EfCore.Specifications.Tests/RepositorySpecTests.cs @@ -132,6 +132,7 @@ await Should.ThrowAsync(async () => public async Task Delete_WithExistingEntity_ShouldMarkForDeletion() { // Arrange + _context.ChangeTracker.Clear(); var categoryId = _context.Categories.First().Id; var product = new Product { Name = "ToDelete", Price = 50m, CategoryId = categoryId }; await _context.Products.AddAsync(product); diff --git a/src/EfCore/EfCore.Specifications.Tests/SpecFilterTests.cs b/src/EfCore/EfCore.Specifications.Tests/SpecFilterTests.cs index 24ae3718..91b1bc30 100644 --- a/src/EfCore/EfCore.Specifications.Tests/SpecFilterTests.cs +++ b/src/EfCore/EfCore.Specifications.Tests/SpecFilterTests.cs @@ -71,7 +71,8 @@ public void ProductFilterSpecification_DefaultOrder_GeneratesOrderByName() var sql = _repository.Query(spec).ToQueryString(); // Assert - sql.ShouldContain("ORDER BY [p].[Name]"); + var normalizedSql = NormalizeSql(sql); + normalizedSql.ShouldContain("ORDER BY p.Name", Case.Insensitive); } [Fact] @@ -94,12 +95,20 @@ public void ProductFilterSpecification_WithSearchAndOrder_GeneratesExpectedSql() _output.WriteLine(sql); // Assert - sql.ShouldContain("[p].[Name] LIKE"); - sql.ShouldContain("[p].[Description] LIKE"); - sql.ShouldContain("ORDER BY [p].[Name]"); - sql.ShouldContain( - "WHERE [p].[IsActive] = CAST(1 AS bit) AND ([p].[Name] LIKE N'%a%' OR [p].[Description] LIKE N'%a%')"); + var normalizedSql = NormalizeSql(sql); + normalizedSql.ShouldContain("where", Case.Insensitive); + normalizedSql.ShouldContain("p.IsActive", Case.Insensitive); + normalizedSql.ShouldContain("p.Name", Case.Insensitive); + normalizedSql.ShouldContain("p.Description", Case.Insensitive); + normalizedSql.ShouldContain("OR", Case.Insensitive); + normalizedSql.ShouldContain("ORDER BY p.Name", Case.Insensitive); } + private static string NormalizeSql(string sql) + => sql + .Replace("\"", string.Empty, StringComparison.Ordinal) + .Replace("[", string.Empty, StringComparison.Ordinal) + .Replace("]", string.Empty, StringComparison.Ordinal); + #endregion } \ No newline at end of file diff --git a/src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs b/src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs index ffe956a8..4f34efef 100644 --- a/src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs +++ b/src/EfCore/EfCore.Specifications.Tests/SpecSetupTests.cs @@ -13,7 +13,7 @@ public void AddSpecRepo_ShouldAllowMultipleRegistrations() // Arrange var services = new ServiceCollection(); services.AddDbContext(options => - options.UseSqlServer("Server=localhost;Database=Test;")); + options.UseSqlite("Data Source=:memory:")); // Act services.AddSpecRepo(); @@ -31,7 +31,7 @@ public void AddSpecRepo_ShouldRegisterAsScopedService() // Arrange var services = new ServiceCollection(); services.AddDbContext(options => - options.UseSqlServer("Server=localhost;Database=Test;")); + options.UseSqlite("Data Source=:memory:")); // Act services.AddSpecRepo(); @@ -48,7 +48,7 @@ public void AddSpecRepo_ShouldRegisterRepositorySpec() // Arrange var services = new ServiceCollection(); services.AddDbContext(options => - options.UseSqlServer("Server=localhost;Database=Test;")); + options.UseSqlite("Data Source=:memory:")); // Act services.AddSpecRepo(); @@ -67,7 +67,7 @@ public void AddSpecRepo_ShouldReturnSameServiceCollection() // Arrange var services = new ServiceCollection(); services.AddDbContext(options => - options.UseSqlServer("Server=localhost;Database=Test;")); + options.UseSqlite("Data Source=:memory:")); // Act var result = services.AddSpecRepo(); @@ -82,7 +82,7 @@ public void AddSpecRepo_WithDifferentDbContext_ShouldRegister() // Arrange var services = new ServiceCollection(); services.AddDbContext(options => - options.UseSqlServer("Server=localhost;Database=Test;")); + options.UseSqlite("Data Source=:memory:")); // Act services.AddSpecRepo();