@danmoisan/drm-copilot-mcp 1.1.19 → 1.1.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +172 -11
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +17 -6
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -2
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +5 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +4 -0
- package/resources/claude-customizations/.claude/agents/task-researcher.md +8 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -53
- package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +5 -1
- package/resources/claude-customizations/.claude/hooks/premortem-section.ps1 +149 -0
- package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +210 -84
- package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +13 -1
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +24 -5
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-MergeConfirmationParser.ps1 +203 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +10 -10
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +46 -10
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +48 -16
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +7 -3
- package/resources/claude-customizations/.claude/skills/csharp-change-budget-router/SKILL.md +2 -2
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +12 -3
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -2
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +134 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +22 -9
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +29 -5
- package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +14 -6
- package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +6 -0
- package/resources/claude-customizations/config/orchestration-routing.json +7 -7
- package/resources/claude-customizations/pack-manifests/core.json +4 -0
- package/resources/codex-and-agents-customizations/.agents/skills/README.md +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +8 -7
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +39 -4
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +8 -6
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +6 -6
- package/resources/codex-and-agents-customizations/.agents/skills/csharp-change-budget-router/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +15 -8
- package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +134 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +16 -6
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell-change-budget-router/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
- package/resources/codex-and-agents-customizations/.agents/skills/research-issue/SKILL.md +6 -0
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -7
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +6 -3
- package/resources/codex-and-agents-customizations/.codex/agents/atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +4 -2
- package/resources/codex-and-agents-customizations/.codex/agents/epic-planner.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +8 -5
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/pr-author.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/prd-feature.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c1.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c2.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3-elevated.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c4.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/task-researcher.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +17 -38
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +5 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +82 -70
- package/resources/codex-and-agents-customizations/.codex/prompts/feature-review-remediate.md +6 -4
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/config/orchestration-routing.json +7 -7
- package/resources/customizations/.github/agents/atomic_executor.agent.md +21 -11
- package/resources/customizations/.github/agents/atomic_planning.agent.md +8 -3
- package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +13 -9
- package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +2 -2
- package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +3 -3
- package/resources/customizations/.github/agents/orchestrator.agent.md +8 -5
- package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +13 -9
- package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +2 -2
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +3 -3
- package/resources/customizations/.github/agents/python-atomic-executor.agent.md +13 -9
- package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +2 -2
- package/resources/customizations/.github/agents/task-researcher.agent.md +6 -0
- package/resources/customizations/.github/prompts/generate-atomic-plan.prompt.md +16 -10
- package/resources/customizations/.github/prompts/orchestrate-csharp-work.prompt.md +2 -2
- package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +1 -1
- package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +1 -1
- package/resources/customizations/.github/prompts/orchestrate-work.prompt.md +2 -2
- package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +39 -4
- package/resources/customizations/.github/skills/csharp-change-budget-router/SKILL.md +1 -1
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +134 -0
- package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +1 -1
- package/resources/customizations/.github/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
- package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +10 -0
- package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +11 -0
- package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +11 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +10 -10
- package/resources/powershell/PoshQC/PoshQC.CoverageMerge.psm1 +163 -0
- package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +296 -0
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +23 -4
- package/resources/powershell/PoshQC/PoshQC.psd1 +7 -1
- package/resources/powershell/PoshQC/PoshQC.psm1 +17 -5
- package/resources/powershell/PoshQC/README.md +13 -1
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +3 -0
|
@@ -36,6 +36,17 @@ Evidence accounting rule: record the expected artifact path or location in each
|
|
|
36
36
|
- [ ] Update docs/tasks/initiative references
|
|
37
37
|
- [ ] Final pass for stray references to old locations
|
|
38
38
|
|
|
39
|
+
## Pre-Mortem
|
|
40
|
+
|
|
41
|
+
<!-- One bullet per label; state how the next review or CI run could fail on that dimension and the mitigation, or "not applicable" with the reason. -->
|
|
42
|
+
- Review checklists:
|
|
43
|
+
- Coverage:
|
|
44
|
+
- Mirror parity:
|
|
45
|
+
- File size:
|
|
46
|
+
- Test isolation:
|
|
47
|
+
- CI-only behavior:
|
|
48
|
+
- Evidence:
|
|
49
|
+
|
|
39
50
|
## Test Plan
|
|
40
51
|
|
|
41
52
|
- Unit/Integration: impacted modules and any regression tests for invariants
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
`scripts/dev_tools/resolve_codex_deployment.py`. It resolves the exact Codex
|
|
8
8
|
deployment agent, model, and reasoning effort for one delegation from the
|
|
9
9
|
logical agent, the assessed complexity band, the execution context, and the
|
|
10
|
-
orchestration complexity ceiling. C3 defaults to
|
|
11
|
-
|
|
12
|
-
planner and epic orchestrator personas are always forced to
|
|
10
|
+
orchestration complexity ceiling. C3 defaults to Sol/high and elevates to
|
|
11
|
+
Astra/high only for an epic child or a C4 orchestration ceiling; the epic
|
|
12
|
+
planner and epic orchestrator personas are always forced to Astra/ultra. No
|
|
13
13
|
model alias and no silent fallback is accepted.
|
|
14
14
|
|
|
15
15
|
SINGLE-IMPLEMENTATION RULE. The orchestrator-state U6.X checks
|
|
@@ -81,20 +81,20 @@ $script:LOGICAL_AGENT_ALIASES = @{ 'feature-review' = 'feature-reviewer' }
|
|
|
81
81
|
|
|
82
82
|
# The per-band deployment profiles (suffix, model, reasoning effort).
|
|
83
83
|
$script:BASE_PROFILES = @{
|
|
84
|
-
C1 = @{ suffix = 'c1'; model = 'gpt-
|
|
85
|
-
C2 = @{ suffix = 'c2'; model = 'gpt-
|
|
86
|
-
C3 = @{ suffix = 'c3'; model = 'gpt-
|
|
87
|
-
C4 = @{ suffix = 'c4'; model = 'gpt-
|
|
84
|
+
C1 = @{ suffix = 'c1'; model = 'gpt-6-luna'; model_reasoning_effort = 'low' }
|
|
85
|
+
C2 = @{ suffix = 'c2'; model = 'gpt-6.1-sol'; model_reasoning_effort = 'medium' }
|
|
86
|
+
C3 = @{ suffix = 'c3'; model = 'gpt-6.1-sol'; model_reasoning_effort = 'high' }
|
|
87
|
+
C4 = @{ suffix = 'c4'; model = 'gpt-6-astra'; model_reasoning_effort = 'max' }
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
# The profile a C3 delegation elevates to when the overlay applies.
|
|
91
|
-
$script:C3_ELEVATED_PROFILE = @{ suffix = 'c3-elevated'; model = 'gpt-
|
|
91
|
+
$script:C3_ELEVATED_PROFILE = @{ suffix = 'c3-elevated'; model = 'gpt-6-astra'; model_reasoning_effort = 'high' }
|
|
92
92
|
|
|
93
93
|
# The personas whose profile is forced regardless of band, context, or ceiling.
|
|
94
94
|
# Their deployment agent is the logical agent itself (empty suffix).
|
|
95
95
|
$script:FORCED_PERSONA_PROFILES = @{
|
|
96
|
-
'epic-planner' = @{ suffix = ''; model = 'gpt-
|
|
97
|
-
'epic-orchestrator' = @{ suffix = ''; model = 'gpt-
|
|
96
|
+
'epic-planner' = @{ suffix = ''; model = 'gpt-6-astra'; model_reasoning_effort = 'ultra' }
|
|
97
|
+
'epic-orchestrator' = @{ suffix = ''; model = 'gpt-6-astra'; model_reasoning_effort = 'ultra' }
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Loads CoverageGutters (JaCoCo-structured) XML text into an XmlDocument (issue #953).
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Disables external resolution, so the DOCTYPE reference to report.dtd is never fetched, and keeps
|
|
6
|
+
whitespace so the merged report serializes with the layout of the first input.
|
|
7
|
+
.PARAMETER CoverageXml
|
|
8
|
+
The XML text of one coverage report.
|
|
9
|
+
#>
|
|
10
|
+
function ConvertFrom-PoshQCCoverageXml {
|
|
11
|
+
[CmdletBinding()]
|
|
12
|
+
[OutputType([System.Xml.XmlDocument])]
|
|
13
|
+
param(
|
|
14
|
+
[Parameter(Mandatory)][string] $CoverageXml
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
$document = New-Object System.Xml.XmlDocument
|
|
18
|
+
$document.XmlResolver = $null
|
|
19
|
+
$document.PreserveWhitespace = $true
|
|
20
|
+
$document.LoadXml($CoverageXml)
|
|
21
|
+
$document
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
<#
|
|
25
|
+
.SYNOPSIS
|
|
26
|
+
Merges the coverage reports written by the shards of one test run (issue #953).
|
|
27
|
+
.DESCRIPTION
|
|
28
|
+
Takes XML text only (no file I/O). Every report must list the same set of
|
|
29
|
+
package/sourcefile/line-number triples, otherwise the merge throws. For each triple the merged
|
|
30
|
+
line carries ci = max(ci) and mi = (mi + ci of the first report) - max(ci). The LINE and
|
|
31
|
+
INSTRUCTION counters of every sourcefile, every package, and the report are then recomputed
|
|
32
|
+
bottom-up: LINE covered counts lines with ci greater than 0 and LINE missed counts lines with ci
|
|
33
|
+
equal to 0; INSTRUCTION covered is the sum of ci and INSTRUCTION missed is the sum of mi.
|
|
34
|
+
Class and method elements and every counter other than LINE and INSTRUCTION are removed, because they
|
|
35
|
+
cannot be recomputed from merged line hits (CR-8). No coverage threshold is enforced. Returns the merged XML text.
|
|
36
|
+
.PARAMETER CoverageXml
|
|
37
|
+
The XML text of each shard's coverage report; at least one is required.
|
|
38
|
+
#>
|
|
39
|
+
function Merge-PoshQCCoverageReport {
|
|
40
|
+
[CmdletBinding()]
|
|
41
|
+
[OutputType([string])]
|
|
42
|
+
param(
|
|
43
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][string[]] $CoverageXml
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if ($CoverageXml.Count -lt 1) {
|
|
47
|
+
throw 'Merge-PoshQCCoverageReport requires at least one coverage report.'
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$indexLines = {
|
|
51
|
+
param([System.Xml.XmlDocument] $Document, [int] $Position)
|
|
52
|
+
$index = New-Object 'System.Collections.Generic.Dictionary[string,System.Xml.XmlElement]' ([System.StringComparer]::Ordinal)
|
|
53
|
+
foreach ($package in $Document.SelectNodes('/report/package')) {
|
|
54
|
+
foreach ($sourceFile in $package.SelectNodes('sourcefile')) {
|
|
55
|
+
foreach ($line in $sourceFile.SelectNodes('line')) {
|
|
56
|
+
$key = '{0}|{1}|{2}' -f $package.GetAttribute('name'), $sourceFile.GetAttribute('name'), $line.GetAttribute('nr')
|
|
57
|
+
if ($index.ContainsKey($key)) {
|
|
58
|
+
throw "Coverage report $Position lists line '$key' more than once."
|
|
59
|
+
}
|
|
60
|
+
$index.Add($key, $line)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
, $index
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
$merged = ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml[0]
|
|
68
|
+
$mergedLines = & $indexLines $merged 1
|
|
69
|
+
for ($position = 1; $position -lt $CoverageXml.Count; $position++) {
|
|
70
|
+
$reportNumber = $position + 1
|
|
71
|
+
$otherLines = & $indexLines (ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml[$position]) $reportNumber
|
|
72
|
+
if ($otherLines.Count -ne $mergedLines.Count) {
|
|
73
|
+
throw "Coverage report $reportNumber lists $($otherLines.Count) lines; report 1 lists $($mergedLines.Count)."
|
|
74
|
+
}
|
|
75
|
+
foreach ($entry in $otherLines.GetEnumerator()) {
|
|
76
|
+
$line = $null
|
|
77
|
+
if (-not $mergedLines.TryGetValue($entry.Key, [ref] $line)) {
|
|
78
|
+
throw "Coverage report $reportNumber lists line '$($entry.Key)', which report 1 does not list."
|
|
79
|
+
}
|
|
80
|
+
$otherCovered = [long] $entry.Value.GetAttribute('ci')
|
|
81
|
+
$currentCovered = [long] $line.GetAttribute('ci')
|
|
82
|
+
if ($otherCovered -gt $currentCovered) {
|
|
83
|
+
$commandTotal = [long] $line.GetAttribute('mi') + $currentCovered
|
|
84
|
+
$line.SetAttribute('ci', [string] $otherCovered)
|
|
85
|
+
$line.SetAttribute('mi', [string] ($commandTotal - $otherCovered))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Issue #953 (CR-8): class and method elements, and counters other than LINE and INSTRUCTION, cannot be
|
|
91
|
+
# recomputed from merged line hits and would keep the figures of report 1, so the merged report omits them.
|
|
92
|
+
foreach ($staleNode in @($merged.SelectNodes("//class | //counter[@type!='LINE' and @type!='INSTRUCTION']"))) {
|
|
93
|
+
[void] $staleNode.ParentNode.RemoveChild($staleNode)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
$setCounter = {
|
|
97
|
+
param([System.Xml.XmlElement] $Node, [string] $Type, [long] $Covered, [long] $Missed)
|
|
98
|
+
$counter = $Node.SelectSingleNode("counter[@type='$Type']")
|
|
99
|
+
if ($counter) {
|
|
100
|
+
$counter.SetAttribute('missed', [string] $Missed)
|
|
101
|
+
$counter.SetAttribute('covered', [string] $Covered)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
$setTotals = {
|
|
105
|
+
param([System.Xml.XmlElement] $Node, [long[]] $Totals)
|
|
106
|
+
& $setCounter $Node 'INSTRUCTION' $Totals[2] $Totals[3]
|
|
107
|
+
& $setCounter $Node 'LINE' $Totals[0] $Totals[1]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Totals layout: 0 = LINE covered, 1 = LINE missed, 2 = INSTRUCTION covered, 3 = INSTRUCTION missed.
|
|
111
|
+
$reportTotals = New-Object 'long[]' 4
|
|
112
|
+
foreach ($package in $merged.DocumentElement.SelectNodes('package')) {
|
|
113
|
+
$packageTotals = New-Object 'long[]' 4
|
|
114
|
+
foreach ($sourceFile in $package.SelectNodes('sourcefile')) {
|
|
115
|
+
$fileTotals = New-Object 'long[]' 4
|
|
116
|
+
foreach ($line in $sourceFile.SelectNodes('line')) {
|
|
117
|
+
$lineCovered = [long] $line.GetAttribute('ci')
|
|
118
|
+
if ($lineCovered -gt 0) { $fileTotals[0]++ } else { $fileTotals[1]++ }
|
|
119
|
+
$fileTotals[2] += $lineCovered
|
|
120
|
+
$fileTotals[3] += [long] $line.GetAttribute('mi')
|
|
121
|
+
}
|
|
122
|
+
& $setTotals $sourceFile $fileTotals
|
|
123
|
+
for ($slot = 0; $slot -lt 4; $slot++) { $packageTotals[$slot] += $fileTotals[$slot] }
|
|
124
|
+
}
|
|
125
|
+
& $setTotals $package $packageTotals
|
|
126
|
+
for ($slot = 0; $slot -lt 4; $slot++) { $reportTotals[$slot] += $packageTotals[$slot] }
|
|
127
|
+
}
|
|
128
|
+
& $setTotals $merged.DocumentElement $reportTotals
|
|
129
|
+
|
|
130
|
+
$merged.OuterXml
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
<#
|
|
134
|
+
.SYNOPSIS
|
|
135
|
+
Summarizes the report-level LINE counter of a coverage report (issue #953).
|
|
136
|
+
.DESCRIPTION
|
|
137
|
+
Returns covered, missed, total, and percent (rounded to two decimals; 0 when total is 0) from the
|
|
138
|
+
report-level LINE counter. Takes XML text only and enforces no threshold.
|
|
139
|
+
.PARAMETER CoverageXml
|
|
140
|
+
The XML text of one coverage report.
|
|
141
|
+
#>
|
|
142
|
+
function Get-PoshQCCoverageLineSummary {
|
|
143
|
+
[CmdletBinding()]
|
|
144
|
+
param(
|
|
145
|
+
[Parameter(Mandatory)][string] $CoverageXml
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
$document = ConvertFrom-PoshQCCoverageXml -CoverageXml $CoverageXml
|
|
149
|
+
$counter = $document.SelectSingleNode("/report/counter[@type='LINE']")
|
|
150
|
+
if (-not $counter) {
|
|
151
|
+
throw 'The coverage report has no report-level LINE counter.'
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
$covered = [long] $counter.GetAttribute('covered')
|
|
155
|
+
$missed = [long] $counter.GetAttribute('missed')
|
|
156
|
+
$total = $covered + $missed
|
|
157
|
+
[pscustomobject]@{
|
|
158
|
+
covered = $covered
|
|
159
|
+
missed = $missed
|
|
160
|
+
total = $total
|
|
161
|
+
percent = if ($total -gt 0) { [math]::Round(100.0 * $covered / $total, 2) } else { 0 }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Selects the test files assigned to one shard (issue #953).
|
|
4
|
+
.DESCRIPTION
|
|
5
|
+
Assigns the files in longest-processing-time order (issue #953, CR-10): heaviest file first, equal weights
|
|
6
|
+
in ordinal path order, each file to the shard with the least assigned weight, then the fewest files, then
|
|
7
|
+
the lowest index, so the shard loads differ by at most the weight of one file. When Weight is omitted every
|
|
8
|
+
file weighs 1, which assigns the files round-robin in ordinal path order. Returns this shard's files in
|
|
9
|
+
input order. Throws when ShardCount is below 1, ShardIndex is outside 1..ShardCount, Weight and TestFile
|
|
10
|
+
differ in length, or a weight is negative (CR-18).
|
|
11
|
+
.PARAMETER TestFile
|
|
12
|
+
Ordered list of test file paths to distribute.
|
|
13
|
+
.PARAMETER ShardIndex
|
|
14
|
+
One-based index of the shard to select.
|
|
15
|
+
.PARAMETER ShardCount
|
|
16
|
+
Total number of shards.
|
|
17
|
+
.PARAMETER Weight
|
|
18
|
+
Optional non-negative weight of each test file (for example its size in bytes), parallel to TestFile.
|
|
19
|
+
#>
|
|
20
|
+
function Get-PoshQCShardSlice {
|
|
21
|
+
[CmdletBinding()]
|
|
22
|
+
[OutputType([string])]
|
|
23
|
+
param(
|
|
24
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][string[]] $TestFile,
|
|
25
|
+
[Parameter(Mandatory)][int] $ShardIndex,
|
|
26
|
+
[Parameter(Mandatory)][int] $ShardCount,
|
|
27
|
+
[long[]] $Weight
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if ($ShardCount -lt 1) {
|
|
31
|
+
throw "ShardCount must be at least 1; received $ShardCount."
|
|
32
|
+
}
|
|
33
|
+
if ($ShardIndex -lt 1 -or $ShardIndex -gt $ShardCount) {
|
|
34
|
+
throw "ShardIndex must be between 1 and $ShardCount; received $ShardIndex."
|
|
35
|
+
}
|
|
36
|
+
if ($null -eq $Weight) {
|
|
37
|
+
$Weight = [long[]] (@(1) * $TestFile.Count)
|
|
38
|
+
}
|
|
39
|
+
if ($Weight.Count -ne $TestFile.Count) {
|
|
40
|
+
throw "Weight lists $($Weight.Count) values; TestFile lists $($TestFile.Count) files."
|
|
41
|
+
}
|
|
42
|
+
for ($position = 0; $position -lt $Weight.Count; $position++) {
|
|
43
|
+
if ($Weight[$position] -lt 0) {
|
|
44
|
+
throw "Weight must not be negative; '$($TestFile[$position])' has weight $($Weight[$position])."
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
$order = [int[]] @(for ($position = 0; $position -lt $TestFile.Count; $position++) { $position })
|
|
49
|
+
$heaviestFirst = [System.Comparison[int]] {
|
|
50
|
+
param([int] $Left, [int] $Right)
|
|
51
|
+
$byWeight = $Weight[$Right].CompareTo($Weight[$Left])
|
|
52
|
+
if ($byWeight -ne 0) { return $byWeight }
|
|
53
|
+
[string]::CompareOrdinal($TestFile[$Left], $TestFile[$Right])
|
|
54
|
+
}
|
|
55
|
+
[System.Array]::Sort($order, $heaviestFirst)
|
|
56
|
+
|
|
57
|
+
$load = New-Object 'long[]' $ShardCount
|
|
58
|
+
$fileCount = New-Object 'int[]' $ShardCount
|
|
59
|
+
$selected = New-Object 'System.Collections.Generic.HashSet[int]'
|
|
60
|
+
foreach ($position in $order) {
|
|
61
|
+
$target = 0
|
|
62
|
+
for ($shard = 1; $shard -lt $ShardCount; $shard++) {
|
|
63
|
+
if ($load[$shard] -lt $load[$target] -or ($load[$shard] -eq $load[$target] -and $fileCount[$shard] -lt $fileCount[$target])) {
|
|
64
|
+
$target = $shard
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
$load[$target] += $Weight[$position]
|
|
68
|
+
$fileCount[$target]++
|
|
69
|
+
if ($target -eq $ShardIndex - 1) {
|
|
70
|
+
[void] $selected.Add($position)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for ($position = 0; $position -lt $TestFile.Count; $position++) {
|
|
75
|
+
if ($selected.Contains($position)) {
|
|
76
|
+
$TestFile[$position]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
<#
|
|
82
|
+
.SYNOPSIS
|
|
83
|
+
Builds the shard manifest for one shard (issue #953).
|
|
84
|
+
.DESCRIPTION
|
|
85
|
+
Returns an in-memory object with shardIndex, shardCount, all (root-relative forward-slash paths
|
|
86
|
+
of every input file), selected (root-relative paths of this shard's slice), and selectedFullPath
|
|
87
|
+
(the slice as given). Relative paths are computed by case-insensitive removal of the root prefix;
|
|
88
|
+
a path outside the root keeps its forward-slash form.
|
|
89
|
+
.PARAMETER RootPath
|
|
90
|
+
Workspace root that the relative paths are computed against.
|
|
91
|
+
.PARAMETER TestFile
|
|
92
|
+
Ordered list of test file paths to distribute.
|
|
93
|
+
.PARAMETER ShardIndex
|
|
94
|
+
One-based index of the shard.
|
|
95
|
+
.PARAMETER ShardCount
|
|
96
|
+
Total number of shards.
|
|
97
|
+
.PARAMETER Weight
|
|
98
|
+
Optional weight of each test file, passed to Get-PoshQCShardSlice.
|
|
99
|
+
#>
|
|
100
|
+
function New-PoshQCShardManifest {
|
|
101
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory factory that changes no system state.')]
|
|
102
|
+
[CmdletBinding()]
|
|
103
|
+
param(
|
|
104
|
+
[Parameter(Mandatory)][string] $RootPath,
|
|
105
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][string[]] $TestFile,
|
|
106
|
+
[Parameter(Mandatory)][int] $ShardIndex,
|
|
107
|
+
[Parameter(Mandatory)][int] $ShardCount,
|
|
108
|
+
[long[]] $Weight
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
$sliceArguments = @{ TestFile = $TestFile; ShardIndex = $ShardIndex; ShardCount = $ShardCount }
|
|
112
|
+
if ($PSBoundParameters.ContainsKey('Weight')) {
|
|
113
|
+
$sliceArguments['Weight'] = $Weight
|
|
114
|
+
}
|
|
115
|
+
$selected = @(Get-PoshQCShardSlice @sliceArguments)
|
|
116
|
+
$prefix = ($RootPath -replace '\\', '/').TrimEnd('/') + '/'
|
|
117
|
+
$toRelative = {
|
|
118
|
+
param([string] $Path)
|
|
119
|
+
$normalized = $Path -replace '\\', '/'
|
|
120
|
+
if ($normalized.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
121
|
+
$normalized.Substring($prefix.Length)
|
|
122
|
+
} else {
|
|
123
|
+
$normalized
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
[pscustomobject]@{
|
|
128
|
+
shardIndex = $ShardIndex
|
|
129
|
+
shardCount = $ShardCount
|
|
130
|
+
all = @($TestFile | ForEach-Object { & $toRelative $_ })
|
|
131
|
+
selected = @($selected | ForEach-Object { & $toRelative $_ })
|
|
132
|
+
selectedFullPath = $selected
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
<#
|
|
137
|
+
.SYNOPSIS
|
|
138
|
+
Verifies that a set of shard manifests covers every test file exactly once (issue #953).
|
|
139
|
+
.DESCRIPTION
|
|
140
|
+
Throws unless the shard indices are exactly 1..N once each, every manifest declares shardCount N,
|
|
141
|
+
every 'all' list is identical, the 'selected' lists are pairwise disjoint, and their union sorted
|
|
142
|
+
ordinally equals 'all' sorted ordinally. Returns $true otherwise.
|
|
143
|
+
.PARAMETER ShardManifest
|
|
144
|
+
The manifests produced by New-PoshQCShardManifest, one per shard (for example loaded from JSON).
|
|
145
|
+
#>
|
|
146
|
+
function Test-PoshQCShardCompleteness {
|
|
147
|
+
[CmdletBinding()]
|
|
148
|
+
[OutputType([bool])]
|
|
149
|
+
param(
|
|
150
|
+
[Parameter(Mandatory)][object[]] $ShardManifest
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
$count = $ShardManifest.Count
|
|
154
|
+
$indices = @($ShardManifest | ForEach-Object { [int] $_.shardIndex } | Sort-Object)
|
|
155
|
+
if (($indices -join ',') -ne ((1..$count) -join ',')) {
|
|
156
|
+
throw "Shard indices '$($indices -join ',')' are not exactly 1..$count."
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
$reference = @($ShardManifest[0].all | ForEach-Object { [string] $_ })
|
|
160
|
+
$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::Ordinal)
|
|
161
|
+
foreach ($manifest in $ShardManifest) {
|
|
162
|
+
if ([int] $manifest.shardCount -ne $count) {
|
|
163
|
+
throw "Shard $($manifest.shardIndex) declares shardCount $($manifest.shardCount); $count manifests were supplied."
|
|
164
|
+
}
|
|
165
|
+
$all = @($manifest.all | ForEach-Object { [string] $_ })
|
|
166
|
+
if (($all -join "`n") -cne ($reference -join "`n")) {
|
|
167
|
+
throw "Shard $($manifest.shardIndex) reports an 'all' list that differs from shard $($ShardManifest[0].shardIndex)."
|
|
168
|
+
}
|
|
169
|
+
foreach ($path in @($manifest.selected | ForEach-Object { [string] $_ })) {
|
|
170
|
+
if (-not $seen.Add($path)) {
|
|
171
|
+
throw "Test file '$path' is selected by more than one shard."
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
[string[]] $union = @($seen)
|
|
177
|
+
[string[]] $expected = $reference
|
|
178
|
+
[System.Array]::Sort($union, [System.StringComparer]::Ordinal)
|
|
179
|
+
[System.Array]::Sort($expected, [System.StringComparer]::Ordinal)
|
|
180
|
+
if (($union -join "`n") -cne ($expected -join "`n")) {
|
|
181
|
+
$missing = @($expected | Where-Object { -not $seen.Contains($_) })
|
|
182
|
+
throw "The shard selections do not cover every test file; missing or extra entries (missing: $($missing -join ', '))."
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
$true
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
<#
|
|
189
|
+
.SYNOPSIS
|
|
190
|
+
Verifies that the shard test counts add up to the discovery count (issue #953).
|
|
191
|
+
.DESCRIPTION
|
|
192
|
+
Throws unless the sum of ShardTestCount equals DiscoveryCount. Returns $true otherwise.
|
|
193
|
+
.PARAMETER ShardTestCount
|
|
194
|
+
The test count reported by each shard (for example the JUnit testsuites/@tests value).
|
|
195
|
+
.PARAMETER DiscoveryCount
|
|
196
|
+
The test count reported by a discovery-only Pester pass over every test file.
|
|
197
|
+
#>
|
|
198
|
+
function Test-PoshQCShardTestCount {
|
|
199
|
+
[CmdletBinding()]
|
|
200
|
+
[OutputType([bool])]
|
|
201
|
+
param(
|
|
202
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][int[]] $ShardTestCount,
|
|
203
|
+
[Parameter(Mandatory)][int] $DiscoveryCount
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
$sum = 0
|
|
207
|
+
foreach ($testCount in $ShardTestCount) {
|
|
208
|
+
$sum += $testCount
|
|
209
|
+
}
|
|
210
|
+
if ($sum -ne $DiscoveryCount) {
|
|
211
|
+
throw "Shard test counts sum to $sum; the discovery pass found $DiscoveryCount tests."
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
$true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
<#
|
|
218
|
+
.SYNOPSIS
|
|
219
|
+
Selects this shard's test files for Invoke-PoshQCTest (issue #953).
|
|
220
|
+
.DESCRIPTION
|
|
221
|
+
Weights each enumerated test file with Get-PoshQCShardTestFileWeight (a FileInfo item, as the default
|
|
222
|
+
EnumerateTests seam returns, weighs the UTF-8 size of its text with CRLF counted as LF; CR-19), builds
|
|
223
|
+
the manifest with New-PoshQCShardManifest, and throws when the shard selects no test file (CR-13), so Pester
|
|
224
|
+
never runs with an empty Run.Path. When ShardFileListPath is set, WriteShardFileList receives the path and
|
|
225
|
+
the manifest JSON without selectedFullPath. Returns the full paths of the selected files.
|
|
226
|
+
.PARAMETER RootPath
|
|
227
|
+
Workspace root that the manifest paths are relative to.
|
|
228
|
+
.PARAMETER TestFile
|
|
229
|
+
Enumerated test files: FileInfo items, objects with FullName (and optionally Length), or path strings.
|
|
230
|
+
.PARAMETER ShardIndex
|
|
231
|
+
One-based index of the shard.
|
|
232
|
+
.PARAMETER ShardCount
|
|
233
|
+
Total number of shards.
|
|
234
|
+
.PARAMETER ShardFileListPath
|
|
235
|
+
Optional path that receives the shard manifest JSON.
|
|
236
|
+
.PARAMETER WriteShardFileList
|
|
237
|
+
Seam that writes the manifest JSON; receives the path and the JSON text.
|
|
238
|
+
#>
|
|
239
|
+
function Select-PoshQCShardTestFile {
|
|
240
|
+
[CmdletBinding()]
|
|
241
|
+
[OutputType([string[]])]
|
|
242
|
+
param(
|
|
243
|
+
[Parameter(Mandatory)][string] $RootPath,
|
|
244
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][object[]] $TestFile,
|
|
245
|
+
[Parameter(Mandatory)][int] $ShardIndex,
|
|
246
|
+
[Parameter(Mandatory)][int] $ShardCount,
|
|
247
|
+
[string] $ShardFileListPath,
|
|
248
|
+
[Parameter(Mandatory)][scriptblock] $WriteShardFileList
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
$paths = [string[]] @($TestFile | ForEach-Object { if ($_ -is [string]) { $_ } else { $_.FullName } })
|
|
252
|
+
$weights = [long[]] @(Get-PoshQCShardTestFileWeight -TestFile $TestFile)
|
|
253
|
+
$manifest = New-PoshQCShardManifest -RootPath $RootPath -TestFile $paths -ShardIndex $ShardIndex -ShardCount $ShardCount -Weight $weights
|
|
254
|
+
if (@($manifest.selected).Count -eq 0) {
|
|
255
|
+
throw "Shard $ShardIndex of $ShardCount selects no test file; $($paths.Count) test files cannot fill $ShardCount shards. Lower the shard count."
|
|
256
|
+
}
|
|
257
|
+
if ($ShardFileListPath) {
|
|
258
|
+
& $WriteShardFileList $ShardFileListPath ($manifest | Select-Object -Property shardIndex, shardCount, all, selected | ConvertTo-Json -Depth 4)
|
|
259
|
+
}
|
|
260
|
+
[string[]] $manifest.selectedFullPath
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
<#
|
|
264
|
+
.SYNOPSIS
|
|
265
|
+
Returns the shard weight of each enumerated test file (issue #953, CR-19).
|
|
266
|
+
.DESCRIPTION
|
|
267
|
+
A FileInfo item weighs the UTF-8 byte count of its text after every CRLF pair is replaced by LF, so a checkout
|
|
268
|
+
with CRLF line endings computes the same weights, and therefore the same shard assignment, as a checkout with
|
|
269
|
+
LF line endings. Another object weighs its Length property when it has one; a plain path string, or an object
|
|
270
|
+
without a Length property, weighs 1. Returns one weight per item, in input order.
|
|
271
|
+
.PARAMETER TestFile
|
|
272
|
+
Enumerated test files: FileInfo items, objects with FullName (and optionally Length), or path strings.
|
|
273
|
+
.PARAMETER ReadTestFileText
|
|
274
|
+
Seam that returns the text of a test file from its full path. Defaults to [System.IO.File]::ReadAllText.
|
|
275
|
+
#>
|
|
276
|
+
function Get-PoshQCShardTestFileWeight {
|
|
277
|
+
[CmdletBinding()]
|
|
278
|
+
[OutputType([long[]])]
|
|
279
|
+
param(
|
|
280
|
+
[Parameter(Mandatory)][AllowEmptyCollection()][object[]] $TestFile,
|
|
281
|
+
[scriptblock] $ReadTestFileText = { param([string] $Path) [System.IO.File]::ReadAllText($Path) }
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
$weights = New-Object 'System.Collections.Generic.List[long]'
|
|
285
|
+
foreach ($item in $TestFile) {
|
|
286
|
+
if ($item -is [System.IO.FileInfo]) {
|
|
287
|
+
$text = [string] (& $ReadTestFileText $item.FullName)
|
|
288
|
+
$weights.Add([System.Text.Encoding]::UTF8.GetByteCount($text.Replace("`r`n", "`n")))
|
|
289
|
+
} elseif ($item -isnot [string] -and $null -ne $item.PSObject.Properties['Length']) {
|
|
290
|
+
$weights.Add([long] $item.Length)
|
|
291
|
+
} else {
|
|
292
|
+
$weights.Add(1)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
[long[]] $weights.ToArray()
|
|
296
|
+
}
|
|
@@ -150,6 +150,14 @@ Optional workspace-relative or workspace-contained folders to scan instead of th
|
|
|
150
150
|
.PARAMETER ResolveCoveragePopulation
|
|
151
151
|
Injectable seam that derives the code-coverage file population from the workspace (issue #527).
|
|
152
152
|
Defaults to Resolve-PoshQCCoveragePopulation.
|
|
153
|
+
.PARAMETER ShardIndex
|
|
154
|
+
One-based shard index; used only when ShardCount is greater than 0 (issue #953).
|
|
155
|
+
.PARAMETER ShardCount
|
|
156
|
+
Number of shards. When greater than 0, Run.Path is set to this shard's size-balanced slice of the test files (Select-PoshQCShardTestFile).
|
|
157
|
+
.PARAMETER ShardFileListPath
|
|
158
|
+
Optional path that receives the shard manifest (shardIndex, shardCount, all, selected) as JSON.
|
|
159
|
+
.PARAMETER WriteShardFileList
|
|
160
|
+
Injectable seam that writes the shard manifest JSON. Defaults to Set-Content.
|
|
153
161
|
#>
|
|
154
162
|
function Invoke-PoshQCTest {
|
|
155
163
|
[CmdletBinding()]
|
|
@@ -160,12 +168,19 @@ function Invoke-PoshQCTest {
|
|
|
160
168
|
[string[]] $ExcludeDirs = $script:DefaultExcludedDirs,
|
|
161
169
|
[string] $KoverageOutputPath,
|
|
162
170
|
[switch] $DisableKoverageCopy,
|
|
171
|
+
[int] $ShardIndex = 0,
|
|
172
|
+
[int] $ShardCount = 0,
|
|
173
|
+
[string] $ShardFileListPath,
|
|
174
|
+
[scriptblock] $WriteShardFileList = { param([string] $Path, [string] $Json) Set-Content -LiteralPath $Path -Value $Json -Encoding utf8 },
|
|
163
175
|
[scriptblock] $EnsureModule = {
|
|
164
|
-
param([string] $Name, [string] $ErrorMessage)
|
|
165
|
-
|
|
176
|
+
param([string] $Name, [string] $ErrorMessage, [string] $MinimumVersion, [string] $MaximumVersion)
|
|
177
|
+
# Issue #953 (CR-1): only an installed version inside the supported range counts as present, so a host
|
|
178
|
+
# whose only Pester is 6.x or the inbox 3.4.0 receives the actionable message instead of an import error.
|
|
179
|
+
if (-not (Get-Module -ListAvailable -FullyQualifiedName @{ ModuleName = $Name; ModuleVersion = $MinimumVersion; MaximumVersion = $MaximumVersion })) { throw $ErrorMessage }
|
|
166
180
|
# Import globally so Pester's Describe/It/Mock resolve in test containers that the
|
|
167
181
|
# default $InvokePester trampoline now hosts in the global session state (issue #392).
|
|
168
|
-
|
|
182
|
+
# Issue #953 (D8, CR-2): -MinimumVersion and -MaximumVersion keep the import inside the supported range.
|
|
183
|
+
Import-Module $Name -Global -MinimumVersion $MinimumVersion -MaximumVersion $MaximumVersion -ErrorAction Stop
|
|
169
184
|
},
|
|
170
185
|
[scriptblock] $TestPathExists = { param([string] $Path) Test-Path $Path },
|
|
171
186
|
[scriptblock] $LoadSettings = { param([string] $Path) Import-PowerShellDataFile -Path $Path },
|
|
@@ -302,7 +317,7 @@ function Invoke-PoshQCTest {
|
|
|
302
317
|
$Root = [IO.Path]::GetFullPath([IO.Path]::Combine($PWD.ProviderPath, $Root))
|
|
303
318
|
}
|
|
304
319
|
|
|
305
|
-
& $EnsureModule 'Pester' "Pester is not installed. Run Install-PoshQCTool (alias Install-PoshQCTools) first."
|
|
320
|
+
& $EnsureModule 'Pester' "Pester is not installed in the supported range (5.9.0 to 5.99.99). Run Install-PoshQCTool (alias Install-PoshQCTools) first." '5.9.0' '5.99.99'
|
|
306
321
|
|
|
307
322
|
if (-not (& $TestPathExists $SettingsPath)) {
|
|
308
323
|
throw "Settings not found: $SettingsPath"
|
|
@@ -393,6 +408,10 @@ function Invoke-PoshQCTest {
|
|
|
393
408
|
& $Logger "No Pester test files found under configured paths for root $Root"
|
|
394
409
|
return
|
|
395
410
|
}
|
|
411
|
+
if ($ShardCount -gt 0) {
|
|
412
|
+
# Issue #953 (FR-09, CR-10, CR-13): run only this shard's size-balanced slice of the enumerated test files.
|
|
413
|
+
$config.Run.Path = [string[]] @(Select-PoshQCShardTestFile -RootPath $Root -TestFile @($testFiles) -ShardIndex $ShardIndex -ShardCount $ShardCount -ShardFileListPath $ShardFileListPath -WriteShardFileList $WriteShardFileList)
|
|
414
|
+
}
|
|
396
415
|
|
|
397
416
|
$pesterResult = & $InvokePester $config
|
|
398
417
|
|
|
@@ -16,7 +16,13 @@
|
|
|
16
16
|
'Invoke-PoshQCAnalyze',
|
|
17
17
|
'Invoke-PoshQCAnalyzeAutofix',
|
|
18
18
|
'Invoke-PoshQCSuite',
|
|
19
|
-
'Invoke-PoshQCTest'
|
|
19
|
+
'Invoke-PoshQCTest',
|
|
20
|
+
'Get-PoshQCShardSlice',
|
|
21
|
+
'Merge-PoshQCCoverageReport',
|
|
22
|
+
'Get-PoshQCCoverageLineSummary',
|
|
23
|
+
'New-PoshQCShardManifest',
|
|
24
|
+
'Test-PoshQCShardCompleteness',
|
|
25
|
+
'Test-PoshQCShardTestCount'
|
|
20
26
|
)
|
|
21
27
|
CmdletsToExport = @()
|
|
22
28
|
VariablesToExport = @()
|
|
@@ -54,11 +54,16 @@ function Install-PoshQCTool {
|
|
|
54
54
|
|
|
55
55
|
$requiredModules = @(
|
|
56
56
|
@{ Name = 'PSScriptAnalyzer'; Version = '1.22.0' },
|
|
57
|
-
|
|
57
|
+
# Issue #953 (D8): UpperBound is exclusive; an installed Pester 6.x does not satisfy the requirement.
|
|
58
|
+
@{ Name = 'Pester'; Version = '5.9.0'; UpperBound = '6.0.0' }
|
|
58
59
|
)
|
|
59
60
|
|
|
61
|
+
# Issue #953 (CR-3): one version-range predicate serves the present check and the post-install check.
|
|
62
|
+
# Where-Object runs it in this function's scope, so $module is the requirement of the current iteration.
|
|
63
|
+
$isSupportedVersion = { $_.Version -ge [version]$module.Version -and (-not $module.ContainsKey('UpperBound') -or $_.Version -lt [version]$module.UpperBound) }
|
|
64
|
+
|
|
60
65
|
foreach ($module in $requiredModules) {
|
|
61
|
-
$installed = & $FindModule $module.Name | Where-Object
|
|
66
|
+
$installed = & $FindModule $module.Name | Where-Object -FilterScript $isSupportedVersion | Select-Object -First 1
|
|
62
67
|
if ($installed) {
|
|
63
68
|
& $Logger "$($module.Name) $($installed.Version) already present."
|
|
64
69
|
continue
|
|
@@ -71,8 +76,7 @@ function Install-PoshQCTool {
|
|
|
71
76
|
throw "Failed to install $($module.Name) $($module.Version): $($_.Exception.Message)"
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
|
|
75
|
-
if (-not $post) {
|
|
79
|
+
if (-not (& $FindModule $module.Name | Where-Object -FilterScript $isSupportedVersion | Select-Object -First 1)) {
|
|
76
80
|
throw "Failed to install $($module.Name) $($module.Version)."
|
|
77
81
|
}
|
|
78
82
|
& $Logger "$($module.Name) $($module.Version) installed."
|
|
@@ -115,6 +119,8 @@ foreach ($subModuleName in @(
|
|
|
115
119
|
'PoshQC.ScanConfig.psm1',
|
|
116
120
|
'PoshQC.Coverage.psm1',
|
|
117
121
|
'PoshQC.Analyzer.psm1',
|
|
122
|
+
'PoshQC.Shard.psm1',
|
|
123
|
+
'PoshQC.CoverageMerge.psm1',
|
|
118
124
|
'PoshQC.Testing.psm1'
|
|
119
125
|
)) {
|
|
120
126
|
$subModulePath = Join-Path $script:ModuleRoot $subModuleName
|
|
@@ -143,6 +149,12 @@ Export-ModuleMember -Function @(
|
|
|
143
149
|
'Invoke-PoshQCSuite',
|
|
144
150
|
'Invoke-PoshQCTest',
|
|
145
151
|
'Convert-PoshQCCoverageToRelative',
|
|
146
|
-
'Get-PoshQCScanConfigFolder'
|
|
152
|
+
'Get-PoshQCScanConfigFolder',
|
|
153
|
+
'Get-PoshQCShardSlice',
|
|
154
|
+
'Merge-PoshQCCoverageReport',
|
|
155
|
+
'Get-PoshQCCoverageLineSummary',
|
|
156
|
+
'New-PoshQCShardManifest',
|
|
157
|
+
'Test-PoshQCShardCompleteness',
|
|
158
|
+
'Test-PoshQCShardTestCount'
|
|
147
159
|
) -Alias @('Install-PoshQCTools')
|
|
148
160
|
|