Skip to content

Commit be419a4

Browse files
committed
updates
1 parent 4f14ada commit be419a4

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

.github/instructions/testing-workflow.instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,4 @@ envConfig.inspect
605605
- **Async initialization in `setImmediate` must have error handling**: When extension activation uses `setImmediate(async () => {...})` for async manager registration, wrap it in try-catch with proper error logging. Unhandled errors cause silent failures where managers never register, making smoke/E2E tests hang forever (1)
606606
- **Never skip tests to hide infrastructure problems**: If tests require native binaries (like `pet`), the CI workflow must build/download them. Skipping tests when infrastructure is missing gives false confidence. Build from source (like vscode-python does) rather than skipping. Tests should fail clearly when something is wrong (2)
607607
- **No retries for masking flakiness**: Mocha `retries` should not be used to mask test flakiness. If a test is flaky, fix the root cause. Retries hide real issues and slow down CI (1)
608+
- **pet binary is required for environment manager registration**: The smoke/E2E/integration tests require the `pet` binary from `microsoft/python-environment-tools` to be built and placed in `python-env-tools/bin/`. Without it, `waitForApiReady()` will timeout because managers never register. CI must build pet from source using `cargo build --release --package pet` (2)

.github/workflows/pr-check.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ jobs:
102102
- name: Install Rust Toolchain
103103
uses: dtolnay/rust-toolchain@stable
104104

105+
- name: Cache Rust build
106+
uses: actions/cache@v4
107+
with:
108+
path: |
109+
~/.cargo/registry
110+
~/.cargo/git
111+
python-env-tools-src/target
112+
key: ${{ runner.os }}-cargo-pet-${{ hashFiles('python-env-tools-src/Cargo.lock') }}
113+
restore-keys: |
114+
${{ runner.os }}-cargo-pet-
115+
105116
- name: Build Python Environment Tools
106117
run: cargo build --release --package pet
107118
working-directory: python-env-tools-src
@@ -184,6 +195,17 @@ jobs:
184195
- name: Install Rust Toolchain
185196
uses: dtolnay/rust-toolchain@stable
186197

198+
- name: Cache Rust build
199+
uses: actions/cache@v4
200+
with:
201+
path: |
202+
~/.cargo/registry
203+
~/.cargo/git
204+
python-env-tools-src/target
205+
key: ${{ runner.os }}-cargo-pet-${{ hashFiles('python-env-tools-src/Cargo.lock') }}
206+
restore-keys: |
207+
${{ runner.os }}-cargo-pet-
208+
187209
- name: Build Python Environment Tools
188210
run: cargo build --release --package pet
189211
working-directory: python-env-tools-src
@@ -266,6 +288,17 @@ jobs:
266288
- name: Install Rust Toolchain
267289
uses: dtolnay/rust-toolchain@stable
268290

291+
- name: Cache Rust build
292+
uses: actions/cache@v4
293+
with:
294+
path: |
295+
~/.cargo/registry
296+
~/.cargo/git
297+
python-env-tools-src/target
298+
key: ${{ runner.os }}-cargo-pet-${{ hashFiles('python-env-tools-src/Cargo.lock') }}
299+
restore-keys: |
300+
${{ runner.os }}-cargo-pet-
301+
269302
- name: Build Python Environment Tools
270303
run: cargo build --release --package pet
271304
working-directory: python-env-tools-src
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import * as assert from 'assert';
5+
import { retryUntilSuccess, sleep, waitForApiReady, waitForCondition } from '../testUtils';
6+
7+
suite('Test Utilities', () => {
8+
suite('sleep', () => {
9+
test('should resolve after specified time', async () => {
10+
const start = Date.now();
11+
await sleep(50);
12+
const elapsed = Date.now() - start;
13+
assert.ok(elapsed >= 45, `Expected at least 45ms, got ${elapsed}ms`);
14+
});
15+
});
16+
17+
suite('waitForCondition', () => {
18+
test('should resolve immediately when condition is true', async () => {
19+
await waitForCondition(() => true, 100, 'Should not fail');
20+
});
21+
22+
test('should resolve when condition becomes true', async () => {
23+
let counter = 0;
24+
await waitForCondition(
25+
() => {
26+
counter++;
27+
return counter >= 3;
28+
},
29+
1000,
30+
'Condition did not become true',
31+
10,
32+
);
33+
assert.ok(counter >= 3, 'Condition should have been checked multiple times');
34+
});
35+
36+
test('should reject when timeout is reached', async () => {
37+
await assert.rejects(
38+
() => waitForCondition(() => false, 100, 'Custom error message', 10),
39+
/Custom error message \(waited 100ms\)/,
40+
);
41+
});
42+
43+
test('should handle async conditions', async () => {
44+
let counter = 0;
45+
await waitForCondition(
46+
async () => {
47+
counter++;
48+
await sleep(5);
49+
return counter >= 2;
50+
},
51+
1000,
52+
'Async condition failed',
53+
10,
54+
);
55+
assert.ok(counter >= 2);
56+
});
57+
58+
test('should continue polling when condition throws', async () => {
59+
let counter = 0;
60+
await waitForCondition(
61+
() => {
62+
counter++;
63+
if (counter < 3) {
64+
throw new Error('Not ready yet');
65+
}
66+
return true;
67+
},
68+
1000,
69+
'Should eventually succeed',
70+
10,
71+
);
72+
assert.ok(counter >= 3);
73+
});
74+
});
75+
76+
suite('retryUntilSuccess', () => {
77+
test('should return result when function succeeds immediately', async () => {
78+
const result = await retryUntilSuccess(
79+
() => 42,
80+
() => true,
81+
1000,
82+
'Should not fail',
83+
);
84+
assert.strictEqual(result, 42);
85+
});
86+
87+
test('should return result when validation passes', async () => {
88+
let counter = 0;
89+
const result = await retryUntilSuccess(
90+
() => {
91+
counter++;
92+
return counter;
93+
},
94+
(val) => val >= 3,
95+
1000,
96+
'Validation failed',
97+
);
98+
assert.ok(result >= 3);
99+
});
100+
101+
test('should reject when timeout reached', async () => {
102+
await assert.rejects(
103+
() =>
104+
retryUntilSuccess(
105+
() => 1,
106+
(val) => val > 10,
107+
100,
108+
'Custom timeout error',
109+
),
110+
/Custom timeout error: validation failed/,
111+
);
112+
});
113+
114+
test('should include last error message in rejection', async () => {
115+
await assert.rejects(
116+
() =>
117+
retryUntilSuccess(
118+
() => {
119+
throw new Error('Specific failure');
120+
},
121+
() => true,
122+
100,
123+
'Operation failed',
124+
),
125+
/Operation failed: Specific failure/,
126+
);
127+
});
128+
129+
test('should handle async functions', async () => {
130+
let counter = 0;
131+
const result = await retryUntilSuccess(
132+
async () => {
133+
counter++;
134+
await sleep(5);
135+
return counter;
136+
},
137+
(val) => val >= 2,
138+
1000,
139+
'Async retry failed',
140+
);
141+
assert.ok(result >= 2);
142+
});
143+
});
144+
145+
suite('waitForApiReady', () => {
146+
test('should return ready:true when getEnvironments succeeds', async () => {
147+
const mockApi = {
148+
getEnvironments: async () => [],
149+
};
150+
const result = await waitForApiReady(mockApi, 1000);
151+
assert.deepStrictEqual(result, { ready: true });
152+
});
153+
154+
test('should return ready:false with error when timeout reached', async () => {
155+
const mockApi = {
156+
getEnvironments: (): Promise<unknown[]> => new Promise(() => {}), // Never resolves
157+
};
158+
const result = await waitForApiReady(mockApi, 100);
159+
assert.strictEqual(result.ready, false);
160+
assert.ok(result.error?.includes('API not ready within 100ms'));
161+
});
162+
163+
test('should return ready:false when getEnvironments throws', async () => {
164+
const mockApi = {
165+
getEnvironments: async () => {
166+
throw new Error('Manager not registered');
167+
},
168+
};
169+
const result = await waitForApiReady(mockApi, 1000);
170+
assert.strictEqual(result.ready, false);
171+
assert.ok(result.error?.includes('Manager not registered'));
172+
});
173+
});
174+
});

0 commit comments

Comments
 (0)