Skip to content

Commit a964025

Browse files
committed
Merge remote-tracking branch 'origin/main' into feature/anycpu-185
# Conflicts: # Reactor.sln
2 parents ac9c9dc + c363bdf commit a964025

33 files changed

Lines changed: 2166 additions & 40 deletions

.github/workflows/ci-stress.yml

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
name: CI Stress
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
iterations:
7+
description: "Total iterations to run (split across shards)"
8+
required: true
9+
default: "100"
10+
type: string
11+
target:
12+
description: "Which test suite(s) to stress"
13+
required: true
14+
default: "both"
15+
type: choice
16+
options:
17+
- unit
18+
- selftests
19+
- both
20+
shards:
21+
description: "Number of parallel runners (iterations are divided across shards)"
22+
required: true
23+
default: "20"
24+
type: string
25+
26+
jobs:
27+
plan:
28+
name: Plan shards
29+
runs-on: ubuntu-latest
30+
outputs:
31+
shard-list: ${{ steps.shards.outputs.list }}
32+
per-shard: ${{ steps.shards.outputs.per-shard }}
33+
remainder: ${{ steps.shards.outputs.remainder }}
34+
steps:
35+
- id: shards
36+
env:
37+
ITERS: ${{ inputs.iterations }}
38+
SHARDS: ${{ inputs.shards }}
39+
run: |
40+
set -euo pipefail
41+
if ! [[ "$ITERS" =~ ^[0-9]+$ ]] || [ "$ITERS" -lt 1 ]; then
42+
echo "iterations must be a positive integer; got '$ITERS'" >&2
43+
exit 1
44+
fi
45+
if ! [[ "$SHARDS" =~ ^[0-9]+$ ]] || [ "$SHARDS" -lt 1 ] || [ "$SHARDS" -gt 256 ]; then
46+
echo "shards must be 1..256; got '$SHARDS'" >&2
47+
exit 1
48+
fi
49+
if [ "$SHARDS" -gt "$ITERS" ]; then
50+
SHARDS="$ITERS"
51+
fi
52+
per=$(( ITERS / SHARDS ))
53+
rem=$(( ITERS % SHARDS ))
54+
list=$(seq 1 "$SHARDS" | jq -R -s -c 'split("\n") | map(select(length>0) | tonumber)')
55+
echo "list=$list" >> "$GITHUB_OUTPUT"
56+
echo "per-shard=$per" >> "$GITHUB_OUTPUT"
57+
echo "remainder=$rem" >> "$GITHUB_OUTPUT"
58+
echo "Plan: $ITERS iterations across $SHARDS shards (~$per each, +1 for first $rem)"
59+
60+
unit-tests:
61+
name: Unit (shard ${{ matrix.shard }})
62+
needs: plan
63+
if: ${{ inputs.target == 'unit' || inputs.target == 'both' }}
64+
runs-on: windows-latest
65+
timeout-minutes: 350
66+
strategy:
67+
fail-fast: false
68+
matrix:
69+
shard: ${{ fromJSON(needs.plan.outputs.shard-list) }}
70+
steps:
71+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
72+
73+
- name: Setup .NET
74+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
75+
with:
76+
dotnet-version: 10.0.x
77+
78+
- name: Restore
79+
run: dotnet restore tests/Reactor.Tests/Reactor.Tests.csproj
80+
81+
- name: Build (once)
82+
run: dotnet build tests/Reactor.Tests/Reactor.Tests.csproj --no-restore --configuration Debug
83+
84+
- name: Stress loop
85+
shell: pwsh
86+
env:
87+
PER_SHARD: ${{ needs.plan.outputs.per-shard }}
88+
REMAINDER: ${{ needs.plan.outputs.remainder }}
89+
SHARD_INDEX: ${{ matrix.shard }}
90+
run: |
91+
$per = [int]$env:PER_SHARD
92+
$rem = [int]$env:REMAINDER
93+
$idx = [int]$env:SHARD_INDEX
94+
$count = if ($idx -le $rem) { $per + 1 } else { $per }
95+
if ($count -lt 1) { Write-Host "Shard $idx has no work."; exit 0 }
96+
$failures = New-Object System.Collections.Generic.List[int]
97+
for ($i = 1; $i -le $count; $i++) {
98+
Write-Host "::group::Unit iteration $i / $count (shard $idx)"
99+
dotnet test tests/Reactor.Tests/Reactor.Tests.csproj --no-restore --no-build --logger "console;verbosity=normal"
100+
$code = $LASTEXITCODE
101+
Write-Host "::endgroup::"
102+
if ($code -ne 0) {
103+
Write-Host "::warning::Unit iteration $i (shard $idx) failed with exit $code"
104+
$failures.Add($i) | Out-Null
105+
}
106+
}
107+
if ($failures.Count -gt 0) {
108+
Write-Host "::error::Shard $idx had $($failures.Count) failed iteration(s): $($failures -join ', ')"
109+
exit 1
110+
}
111+
Write-Host "Shard ${idx}: $count iterations passed."
112+
113+
selftests:
114+
name: Selftests (shard ${{ matrix.shard }})
115+
needs: plan
116+
if: ${{ inputs.target == 'selftests' || inputs.target == 'both' }}
117+
runs-on: windows-latest
118+
timeout-minutes: 350
119+
strategy:
120+
fail-fast: false
121+
matrix:
122+
shard: ${{ fromJSON(needs.plan.outputs.shard-list) }}
123+
steps:
124+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
125+
126+
- name: Setup .NET
127+
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
128+
with:
129+
dotnet-version: 10.0.x
130+
131+
- name: Restore
132+
run: dotnet restore tests/Reactor.SelfTests/Reactor.SelfTests.csproj
133+
134+
- name: Build (once)
135+
run: dotnet build tests/Reactor.SelfTests/Reactor.SelfTests.csproj --no-restore --configuration Debug
136+
137+
- name: Stress loop
138+
shell: pwsh
139+
env:
140+
PER_SHARD: ${{ needs.plan.outputs.per-shard }}
141+
REMAINDER: ${{ needs.plan.outputs.remainder }}
142+
SHARD_INDEX: ${{ matrix.shard }}
143+
run: |
144+
$per = [int]$env:PER_SHARD
145+
$rem = [int]$env:REMAINDER
146+
$idx = [int]$env:SHARD_INDEX
147+
$count = if ($idx -le $rem) { $per + 1 } else { $per }
148+
if ($count -lt 1) { Write-Host "Shard $idx has no work."; exit 0 }
149+
$failures = New-Object System.Collections.Generic.List[int]
150+
for ($i = 1; $i -le $count; $i++) {
151+
Write-Host "::group::Selftest iteration $i / $count (shard $idx)"
152+
dotnet test tests/Reactor.SelfTests/Reactor.SelfTests.csproj --no-restore --no-build --logger "console;verbosity=normal"
153+
$code = $LASTEXITCODE
154+
Write-Host "::endgroup::"
155+
if ($code -ne 0) {
156+
Write-Host "::warning::Selftest iteration $i (shard $idx) failed with exit $code"
157+
$failures.Add($i) | Out-Null
158+
}
159+
}
160+
if ($failures.Count -gt 0) {
161+
Write-Host "::error::Shard $idx had $($failures.Count) failed iteration(s): $($failures -join ', ')"
162+
exit 1
163+
}
164+
Write-Host "Shard ${idx}: $count iterations passed."
165+
166+
summary:
167+
name: Stress summary
168+
needs: [plan, unit-tests, selftests]
169+
if: ${{ always() }}
170+
runs-on: ubuntu-latest
171+
steps:
172+
- name: Report
173+
env:
174+
UNIT_RESULT: ${{ needs.unit-tests.result }}
175+
SELFTESTS_RESULT: ${{ needs.selftests.result }}
176+
run: |
177+
echo "Unit shards: $UNIT_RESULT"
178+
echo "Selftest shards: $SELFTESTS_RESULT"
179+
fail=0
180+
# 'skipped' is OK (target filter); 'success' is OK; anything else is a fail.
181+
for r in "$UNIT_RESULT" "$SELFTESTS_RESULT"; do
182+
case "$r" in
183+
success|skipped|"") ;;
184+
*) fail=1 ;;
185+
esac
186+
done
187+
if [ "$fail" -eq 1 ]; then
188+
echo "::error::One or more stress shards failed. See per-shard logs."
189+
exit 1
190+
fi
191+
echo "All shards passed."

.github/workflows/release.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ jobs:
103103
-p:Version=${{ steps.version.outputs.version }}
104104
-o artifacts/nupkg
105105
106+
- name: Pack Templates
107+
run: >
108+
dotnet pack tools\Templates\Microsoft.UI.Reactor.Templates.csproj
109+
--no-build --configuration Release
110+
-p:Version=${{ steps.version.outputs.version }}
111+
-p:Platform=AnyCPU
112+
-o artifacts/nupkg
113+
106114
# Framework-dependent: the consumer's machine supplies the .NET 10
107115
# runtime — saves ~70 MB per RID. install-skill-kit.ps1 checks for it
108116
# and errors out with an install hint if it's missing. See spec 022 §6.

Reactor.slnx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,10 @@
303303
<Project Path="tests/stress_perf/StressPerf.Shared/StressPerf.Shared.csproj" />
304304
<Project Path="tests/stress_perf/StressPerf.Wpf/StressPerf.Wpf.csproj" />
305305
</Folder>
306+
<Folder Name="/tools/" />
307+
<Folder Name="/tools/Templates/">
308+
<Project Path="tools/Templates/Microsoft.UI.Reactor.Templates.csproj" />
309+
</Folder>
306310
<Project Path="src/Reactor.Analyzers/Reactor.Analyzers.csproj" />
307311
<Project Path="src/Reactor.Cli/Reactor.Cli.csproj">
308312
<Platform Solution="*|ARM64" Project="ARM64" />

docs/_pipeline/apps/components/App.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ public override Element Render()
9797
}
9898
// </snippet:function-component>
9999

100+
// <snippet:factory-helpers>
101+
static class Components
102+
{
103+
public static ComponentElement Alert(string title, string message,
104+
string severity = "info") =>
105+
Component<global::Alert, AlertProps>(new(title, message, severity));
106+
}
107+
// </snippet:factory-helpers>
108+
100109
// <snippet:composition>
101110
class ComponentsApp : Component
102111
{

docs/_pipeline/templates/components.md.dt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,37 @@ changed structurally, the child skips its `Render()` call.
4747
Access props via `Props.PropertyName` inside `Render()`. The parent sets
4848
props by assigning the `Props` property when creating the component instance.
4949

50+
## Factory Helpers for Cleaner Call Sites
51+
52+
`Component<T, TProps>(new(...))` reads heavily at the call site, especially
53+
when nested in an element tree. The idiomatic Reactor pattern is to wrap each
54+
class component in a free-function factory that matches the rest of the DSL:
55+
56+
```csharp snippet="components/factory-helpers"
57+
```
58+
59+
With `using static Components;` at the top of the consuming file, the call
60+
site collapses to a normal function call:
61+
62+
| Before | After |
63+
|-------------------------------------------------------|--------------------------------------|
64+
| `Component<Alert, AlertProps>(new("Saved", "Done"))` | `Alert("Saved", "Done")` |
65+
| `Component<Alert, AlertProps>(new("Hi", "x", "warn"))`| `Alert("Hi", "x", "warn")` |
66+
67+
The class `Alert` and the helper method `Alert` coexist in the same scope —
68+
C# resolves `Alert(args)` as a method call and `Component<Alert, ...>` as a
69+
type reference, based on syntactic position.
70+
71+
Conventions:
72+
73+
- **Match the component name.** Helper `Alert` for class `Alert` reads like
74+
JSX: `Alert(...)` instead of `<Alert ... />`.
75+
- **Group helpers in a single static class** named `Components` (or per
76+
feature). One `using static` import per consuming file is plenty.
77+
- **Skip `Create` / `Of` / `New` prefixes.** Reactor's built-in element
78+
factories (`Button`, `TextBlock`, `FlexRow`) all read as bare functions;
79+
user helpers should match that grammar.
80+
5081
## Custom ShouldUpdate
5182

5283
Override `ShouldUpdate` to control when a component re-renders:

docs/guide/components.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,43 @@ changed structurally, the child skips its `Render()` call.
7070
Access props via `Props.PropertyName` inside `Render()`. The parent sets
7171
props by assigning the `Props` property when creating the component instance.
7272

73+
## Factory Helpers for Cleaner Call Sites
74+
75+
`Component<T, TProps>(new(...))` reads heavily at the call site, especially
76+
when nested in an element tree. The idiomatic Reactor pattern is to wrap each
77+
class component in a free-function factory that matches the rest of the DSL:
78+
79+
```csharp
80+
static class Components
81+
{
82+
public static ComponentElement Alert(string title, string message,
83+
string severity = "info") =>
84+
Component<global::Alert, AlertProps>(new(title, message, severity));
85+
}
86+
```
87+
88+
With `using static Components;` at the top of the consuming file, the call
89+
site collapses to a normal function call:
90+
91+
| Before | After |
92+
|-------------------------------------------------------|--------------------------------------|
93+
| `Component<Alert, AlertProps>(new("Saved", "Done"))` | `Alert("Saved", "Done")` |
94+
| `Component<Alert, AlertProps>(new("Hi", "x", "warn"))`| `Alert("Hi", "x", "warn")` |
95+
96+
The class `Alert` and the helper method `Alert` coexist in the same scope —
97+
C# resolves `Alert(args)` as a method call and `Component<Alert, ...>` as a
98+
type reference, based on syntactic position.
99+
100+
Conventions:
101+
102+
- **Match the component name.** Helper `Alert` for class `Alert` reads like
103+
JSX: `Alert(...)` instead of `<Alert ... />`.
104+
- **Group helpers in a single static class** named `Components` (or per
105+
feature). One `using static` import per consuming file is plenty.
106+
- **Skip `Create` / `Of` / `New` prefixes.** Reactor's built-in element
107+
factories (`Button`, `TextBlock`, `FlexRow`) all read as bare functions;
108+
user helpers should match that grammar.
109+
73110
## Custom ShouldUpdate
74111

75112
Override `ShouldUpdate` to control when a component re-renders:
27 Bytes
Loading
62.1 KB
Loading

0 commit comments

Comments
 (0)