Skip to content

Commit 3cf5fe2

Browse files
authored
5066 unable to inject data between workers because of proxy object (#5072)
* fix share workers * fix share workers
1 parent a27b99e commit 3cf5fe2

File tree

8 files changed

+269
-19
lines changed

8 files changed

+269
-19
lines changed

docs/parallel.md

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -364,37 +364,78 @@ workers.on(event.all.result, (status, completedTests, workerStats) => {
364364
365365
## Sharing Data Between Workers
366366
367-
NodeJS Workers can communicate between each other via messaging system. It may happen that you want to pass some data from one of the workers to other. For instance, you may want to share user credentials accross all tests. Data will be appended to a container.
367+
NodeJS Workers can communicate between each other via messaging system. CodeceptJS allows you to share data between different worker processes using the `share()` and `inject()` functions.
368368
369-
However, you can't access uninitialized data from a container, so to start, you need to initialize data first. Inside `bootstrap` function of the config we execute the `share` to initialize value:
369+
### Basic Usage
370370
371+
You can share data directly using the `share()` function and access it using `inject()`:
372+
373+
```js
374+
// In one test or worker
375+
share({ userData: { name: 'user', password: '123456' } });
376+
377+
// In another test or worker
378+
const testData = inject();
379+
console.log(testData.userData.name); // 'user'
380+
console.log(testData.userData.password); // '123456'
381+
```
382+
383+
### Initializing Data in Bootstrap
384+
385+
For complex scenarios where you need to initialize shared data before tests run, you can use the bootstrap function:
371386
372387
```js
373388
// inside codecept.conf.js
374389
exports.config = {
375390
bootstrap() {
376-
// append empty userData to container
377-
share({ userData: false });
391+
// Initialize shared data container
392+
share({ userData: null, config: { retries: 3 } });
378393
}
379394
}
380395
```
381396
382-
Now each worker has `userData` inside a container. However, it is empty.
383-
When you obtain real data in one of the tests you can now `share` this data accross tests. Use `inject` function to access data inside a container:
397+
Then in your tests, you can check and update the shared data:
384398
385399
```js
386-
// get current value of userData
387-
let { userData } = inject();
388-
// if userData is still empty - update it
389-
if (!userData) {
390-
userData = { name: 'user', password: '123456' };
391-
// now new userData will be shared accross all workers
392-
share({userData : userData});
400+
const testData = inject();
401+
if (!testData.userData) {
402+
// Update shared data - both approaches work:
403+
share({ userData: { name: 'user', password: '123456' } });
404+
// or mutate the injected object:
405+
testData.userData = { name: 'user', password: '123456' };
393406
}
394407
```
395408
396-
If you want to share data only within same worker, and not across all workers, you need to add option `local: true` every time you run `share`
409+
### Working with Proxy Objects
410+
411+
Since CodeceptJS 3.7.0+, shared data uses Proxy objects for synchronization between workers. The proxy system works seamlessly for most use cases:
412+
413+
```js
414+
// ✅ All of these work correctly:
415+
const data = inject();
416+
console.log(data.userData.name); // Access nested properties
417+
console.log(Object.keys(data)); // Enumerate shared keys
418+
data.newProperty = 'value'; // Add new properties
419+
Object.assign(data, { more: 'data' }); // Merge objects
420+
```
421+
422+
**Important Note:** Avoid reassigning the entire injected object:
423+
424+
```js
425+
// ❌ AVOID: This breaks the proxy reference
426+
let testData = inject();
427+
testData = someOtherObject; // This will NOT work as expected!
428+
429+
// ✅ PREFERRED: Use share() to replace data or mutate properties
430+
share({ userData: someOtherObject }); // This works!
431+
// or
432+
Object.assign(inject(), someOtherObject); // This works!
433+
```
434+
435+
### Local Data (Worker-Specific)
436+
437+
If you want to share data only within the same worker (not across all workers), use the `local` option:
397438
398439
```js
399-
share({ userData: false }, {local: true });
440+
share({ localData: 'worker-specific' }, { local: true });
400441
```

lib/container.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ let container = {
2828
translation: {},
2929
/** @type {Result | null} */
3030
result: null,
31+
sharedKeys: new Set() // Track keys shared via share() function
3132
}
3233

3334
/**
@@ -174,6 +175,7 @@ class Container {
174175
container.translation = loadTranslation()
175176
container.proxySupport = createSupportObjects(newSupport)
176177
container.plugins = newPlugins
178+
container.sharedKeys = new Set() // Clear shared keys
177179
asyncHelperPromise = Promise.resolve()
178180
store.actor = null
179181
debug('container cleared')
@@ -197,7 +199,13 @@ class Container {
197199
* @param {Object} options - set {local: true} to not share among workers
198200
*/
199201
static share(data, options = {}) {
200-
Container.append({ support: data })
202+
// Instead of using append which replaces the entire container,
203+
// directly update the support object to maintain proxy references
204+
Object.assign(container.support, data)
205+
206+
// Track which keys were explicitly shared
207+
Object.keys(data).forEach(key => container.sharedKeys.add(key))
208+
201209
if (!options.local) {
202210
WorkerStorage.share(data)
203211
}
@@ -396,10 +404,11 @@ function createSupportObjects(config) {
396404
{},
397405
{
398406
has(target, key) {
399-
return keys.includes(key)
407+
return keys.includes(key) || container.sharedKeys.has(key)
400408
},
401409
ownKeys() {
402-
return keys
410+
// Return both original config keys and explicitly shared keys
411+
return [...new Set([...keys, ...container.sharedKeys])]
403412
},
404413
getOwnPropertyDescriptor(target, prop) {
405414
return {
@@ -409,6 +418,10 @@ function createSupportObjects(config) {
409418
}
410419
},
411420
get(target, key) {
421+
// First check if this is an explicitly shared property
422+
if (container.sharedKeys.has(key) && key in container.support) {
423+
return container.support[key]
424+
}
412425
return lazyLoad(key)
413426
},
414427
},

lib/workerStorage.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => {
77
const { threadId } = workerObj;
88
workerObj.on('message', (messageData) => {
99
if (messageData.event === shareEvent) {
10-
share(messageData.data);
10+
const Container = require('./container');
11+
Container.share(messageData.data);
1112
}
1213
});
1314
workerObj.on('exit', () => {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Test Suite for Issue #5066 Fix
2+
3+
This directory contains tests that validate the fix for **Issue #5066: Unable to inject data between workers because of proxy object**.
4+
5+
## Test Files
6+
7+
### `proxy_test.js`
8+
Basic tests that verify the core functionality of `share()` and `inject()` functions:
9+
- Basic data sharing with primitive types (strings, numbers)
10+
- Complex nested data structures (objects, arrays)
11+
- Property access patterns that should work after the fix
12+
13+
### `final_test.js`
14+
Comprehensive end-to-end validation test that covers:
15+
- Multiple data types and structures
16+
- Data overriding scenarios
17+
- Deep nested property access
18+
- Key enumeration functionality
19+
- Real-world usage patterns
20+
21+
## Running the Tests
22+
23+
### Single-threaded execution:
24+
```bash
25+
npx codeceptjs run proxy_test.js
26+
npx codeceptjs run final_test.js
27+
```
28+
29+
### Multi-worker execution (tests worker communication):
30+
```bash
31+
npx codeceptjs run-workers 2 proxy_test.js
32+
npx codeceptjs run-workers 2 final_test.js
33+
```
34+
35+
## What the Fix Addresses
36+
37+
1. **Circular Dependency Error**: Fixed "Support object undefined is not defined" error in `workerStorage.js`
38+
2. **Proxy System Enhancement**: Updated container proxy system to handle dynamically shared data
39+
3. **Worker Communication**: Ensured data sharing works correctly between worker threads
40+
4. **Key Enumeration**: Made sure `Object.keys(inject())` shows shared properties
41+
42+
## Expected Results
43+
44+
All tests should pass in both single-threaded and multi-worker modes, demonstrating that:
45+
- `share({ data })` correctly shares data between workers
46+
- `inject()` returns a proxy object with proper access to shared data
47+
- Both direct property access and nested object traversal work correctly
48+
- Key enumeration shows all shared properties
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
exports.config = {
2+
tests: './proxy_test.js',
3+
output: './output',
4+
helpers: {
5+
FileSystem: {}
6+
},
7+
include: {},
8+
mocha: {},
9+
name: 'workers-proxy-issue',
10+
};
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const assert = require('assert');
2+
3+
Feature('Complete validation for issue #5066 fix');
4+
5+
Scenario('End-to-end worker data sharing validation', () => {
6+
console.log('=== Testing complete data sharing workflow ===');
7+
8+
// Test 1: Basic data sharing
9+
share({
10+
message: 'Hello from main thread',
11+
config: { timeout: 5000, retries: 3 },
12+
users: ['alice', 'bob', 'charlie']
13+
});
14+
15+
const data = inject();
16+
17+
// Verify all property types work correctly
18+
assert.strictEqual(data.message, 'Hello from main thread', 'String property should work');
19+
assert.strictEqual(data.config.timeout, 5000, 'Nested object property should work');
20+
assert.strictEqual(data.config.retries, 3, 'Nested object property should work');
21+
assert(Array.isArray(data.users), 'Array property should work');
22+
assert.strictEqual(data.users.length, 3, 'Array length should work');
23+
assert.strictEqual(data.users[0], 'alice', 'Array access should work');
24+
25+
// Test 2: Data overriding
26+
share({ message: 'Updated message' });
27+
const updatedData = inject();
28+
assert.strictEqual(updatedData.message, 'Updated message', 'Data override should work');
29+
assert.strictEqual(updatedData.config.timeout, 5000, 'Previous data should persist');
30+
31+
// Test 3: Complex nested structures
32+
share({
33+
testSuite: {
34+
name: 'E2E Tests',
35+
tests: [
36+
{ name: 'Login test', status: 'passed', data: { user: 'admin', pass: 'secret' } },
37+
{ name: 'Checkout test', status: 'failed', error: 'Timeout occurred' }
38+
],
39+
metadata: {
40+
browser: 'chrome',
41+
version: '91.0',
42+
viewport: { width: 1920, height: 1080 }
43+
}
44+
}
45+
});
46+
47+
const complexData = inject();
48+
assert.strictEqual(complexData.testSuite.name, 'E2E Tests', 'Deep nested string should work');
49+
assert.strictEqual(complexData.testSuite.tests[0].data.user, 'admin', 'Very deep nested access should work');
50+
assert.strictEqual(complexData.testSuite.metadata.viewport.width, 1920, 'Very deep nested number should work');
51+
52+
// Test 4: Key enumeration
53+
const allKeys = Object.keys(inject());
54+
assert(allKeys.includes('message'), 'Keys should include shared properties');
55+
assert(allKeys.includes('testSuite'), 'Keys should include all shared properties');
56+
57+
console.log('✅ ALL TESTS PASSED - Issue #5066 is completely fixed!');
58+
console.log('✅ Workers can now share and inject data without circular dependency errors');
59+
console.log('✅ Proxy objects work correctly for both direct and nested property access');
60+
});
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const assert = require('assert');
2+
3+
Feature('Fix for issue #5066: Unable to inject data between workers because of proxy object');
4+
5+
Scenario('Basic share and inject functionality', () => {
6+
console.log('Testing basic share() and inject() functionality...');
7+
8+
// This is the basic pattern that should work after the fix
9+
const originalData = { message: 'Hello', count: 42 };
10+
share(originalData);
11+
12+
const injectedData = inject();
13+
console.log('Shared data keys:', Object.keys(originalData));
14+
console.log('Injected data keys:', Object.keys(injectedData));
15+
16+
// These assertions should pass after the fix
17+
assert.strictEqual(injectedData.message, 'Hello', 'String property should be accessible');
18+
assert.strictEqual(injectedData.count, 42, 'Number property should be accessible');
19+
20+
console.log('✅ SUCCESS: Basic share/inject works!');
21+
});
22+
23+
Scenario('Complex nested data structures', () => {
24+
console.log('Testing complex nested data sharing...');
25+
26+
const testDataJson = {
27+
users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }],
28+
settings: { theme: 'dark', language: 'en' }
29+
};
30+
31+
share({ testDataJson });
32+
33+
const data = inject();
34+
35+
// These should work after the fix
36+
assert(data.testDataJson, 'testDataJson should be accessible');
37+
assert(Array.isArray(data.testDataJson.users), 'users should be an array');
38+
assert.strictEqual(data.testDataJson.users[0].name, 'John', 'Should access nested user data');
39+
assert.strictEqual(data.testDataJson.settings.theme, 'dark', 'Should access nested settings');
40+
41+
console.log('✅ SUCCESS: Complex nested data works!');
42+
});

test/unit/workerStorage_test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const { expect } = require('expect');
2+
const WorkerStorage = require('../../lib/workerStorage');
3+
const { Worker } = require('worker_threads');
4+
const event = require('../../lib/event');
5+
6+
describe('WorkerStorage', () => {
7+
it('should handle share message correctly without circular dependency', (done) => {
8+
// Create a mock worker to test the functionality
9+
const mockWorker = {
10+
threadId: 'test-thread-1',
11+
on: (eventName, callback) => {
12+
if (eventName === 'message') {
13+
// Simulate receiving a share message
14+
setTimeout(() => {
15+
callback({ event: 'share', data: { testKey: 'testValue' } });
16+
done();
17+
}, 10);
18+
}
19+
},
20+
postMessage: () => {}
21+
};
22+
23+
// Add the mock worker to storage
24+
WorkerStorage.addWorker(mockWorker);
25+
});
26+
27+
it('should not crash when sharing data', () => {
28+
const testData = { user: 'test', password: '123' };
29+
30+
// This should not throw an error
31+
expect(() => {
32+
WorkerStorage.share(testData);
33+
}).not.toThrow();
34+
});
35+
});

0 commit comments

Comments
 (0)