Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 56 additions & 15 deletions docs/parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,37 +364,78 @@ workers.on(event.all.result, (status, completedTests, workerStats) => {

## Sharing Data Between Workers

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.
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.

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:
### Basic Usage

You can share data directly using the `share()` function and access it using `inject()`:

```js
// In one test or worker
share({ userData: { name: 'user', password: '123456' } });

// In another test or worker
const testData = inject();
console.log(testData.userData.name); // 'user'
console.log(testData.userData.password); // '123456'
```

### Initializing Data in Bootstrap

For complex scenarios where you need to initialize shared data before tests run, you can use the bootstrap function:

```js
// inside codecept.conf.js
exports.config = {
bootstrap() {
// append empty userData to container
share({ userData: false });
// Initialize shared data container
share({ userData: null, config: { retries: 3 } });
}
}
```

Now each worker has `userData` inside a container. However, it is empty.
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:
Then in your tests, you can check and update the shared data:

```js
// get current value of userData
let { userData } = inject();
// if userData is still empty - update it
if (!userData) {
userData = { name: 'user', password: '123456' };
// now new userData will be shared accross all workers
share({userData : userData});
const testData = inject();
if (!testData.userData) {
// Update shared data - both approaches work:
share({ userData: { name: 'user', password: '123456' } });
// or mutate the injected object:
testData.userData = { name: 'user', password: '123456' };
}
```

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`
### Working with Proxy Objects

Since CodeceptJS 3.7.0+, shared data uses Proxy objects for synchronization between workers. The proxy system works seamlessly for most use cases:

```js
// ✅ All of these work correctly:
const data = inject();
console.log(data.userData.name); // Access nested properties
console.log(Object.keys(data)); // Enumerate shared keys
data.newProperty = 'value'; // Add new properties
Object.assign(data, { more: 'data' }); // Merge objects
```

**Important Note:** Avoid reassigning the entire injected object:

```js
// ❌ AVOID: This breaks the proxy reference
let testData = inject();
testData = someOtherObject; // This will NOT work as expected!

// ✅ PREFERRED: Use share() to replace data or mutate properties
share({ userData: someOtherObject }); // This works!
// or
Object.assign(inject(), someOtherObject); // This works!
```

### Local Data (Worker-Specific)

If you want to share data only within the same worker (not across all workers), use the `local` option:

```js
share({ userData: false }, {local: true });
share({ localData: 'worker-specific' }, { local: true });
```
19 changes: 16 additions & 3 deletions lib/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ let container = {
translation: {},
/** @type {Result | null} */
result: null,
sharedKeys: new Set() // Track keys shared via share() function
}

/**
Expand Down Expand Up @@ -174,6 +175,7 @@ class Container {
container.translation = loadTranslation()
container.proxySupport = createSupportObjects(newSupport)
container.plugins = newPlugins
container.sharedKeys = new Set() // Clear shared keys
asyncHelperPromise = Promise.resolve()
store.actor = null
debug('container cleared')
Expand All @@ -197,7 +199,13 @@ class Container {
* @param {Object} options - set {local: true} to not share among workers
*/
static share(data, options = {}) {
Container.append({ support: data })
// Instead of using append which replaces the entire container,
// directly update the support object to maintain proxy references
Object.assign(container.support, data)

// Track which keys were explicitly shared
Object.keys(data).forEach(key => container.sharedKeys.add(key))

if (!options.local) {
WorkerStorage.share(data)
}
Expand Down Expand Up @@ -396,10 +404,11 @@ function createSupportObjects(config) {
{},
{
has(target, key) {
return keys.includes(key)
return keys.includes(key) || container.sharedKeys.has(key)
},
ownKeys() {
return keys
// Return both original config keys and explicitly shared keys
return [...new Set([...keys, ...container.sharedKeys])]
},
getOwnPropertyDescriptor(target, prop) {
return {
Expand All @@ -409,6 +418,10 @@ function createSupportObjects(config) {
}
},
get(target, key) {
// First check if this is an explicitly shared property
if (container.sharedKeys.has(key) && key in container.support) {
return container.support[key]
}
return lazyLoad(key)
},
},
Expand Down
3 changes: 2 additions & 1 deletion lib/workerStorage.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ const invokeWorkerListeners = (workerObj) => {
const { threadId } = workerObj;
workerObj.on('message', (messageData) => {
if (messageData.event === shareEvent) {
share(messageData.data);
const Container = require('./container');
Container.share(messageData.data);
}
});
workerObj.on('exit', () => {
Expand Down
48 changes: 48 additions & 0 deletions test/data/sandbox/workers-proxy-issue/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Test Suite for Issue #5066 Fix

This directory contains tests that validate the fix for **Issue #5066: Unable to inject data between workers because of proxy object**.

## Test Files

### `proxy_test.js`
Basic tests that verify the core functionality of `share()` and `inject()` functions:
- Basic data sharing with primitive types (strings, numbers)
- Complex nested data structures (objects, arrays)
- Property access patterns that should work after the fix

### `final_test.js`
Comprehensive end-to-end validation test that covers:
- Multiple data types and structures
- Data overriding scenarios
- Deep nested property access
- Key enumeration functionality
- Real-world usage patterns

## Running the Tests

### Single-threaded execution:
```bash
npx codeceptjs run proxy_test.js
npx codeceptjs run final_test.js
```

### Multi-worker execution (tests worker communication):
```bash
npx codeceptjs run-workers 2 proxy_test.js
npx codeceptjs run-workers 2 final_test.js
```

## What the Fix Addresses

1. **Circular Dependency Error**: Fixed "Support object undefined is not defined" error in `workerStorage.js`
2. **Proxy System Enhancement**: Updated container proxy system to handle dynamically shared data
3. **Worker Communication**: Ensured data sharing works correctly between worker threads
4. **Key Enumeration**: Made sure `Object.keys(inject())` shows shared properties

## Expected Results

All tests should pass in both single-threaded and multi-worker modes, demonstrating that:
- `share({ data })` correctly shares data between workers
- `inject()` returns a proxy object with proper access to shared data
- Both direct property access and nested object traversal work correctly
- Key enumeration shows all shared properties
10 changes: 10 additions & 0 deletions test/data/sandbox/workers-proxy-issue/codecept.conf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
exports.config = {
tests: './proxy_test.js',
output: './output',
helpers: {
FileSystem: {}
},
include: {},
mocha: {},
name: 'workers-proxy-issue',
};
60 changes: 60 additions & 0 deletions test/data/sandbox/workers-proxy-issue/final_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const assert = require('assert');

Feature('Complete validation for issue #5066 fix');

Scenario('End-to-end worker data sharing validation', () => {
console.log('=== Testing complete data sharing workflow ===');

// Test 1: Basic data sharing
share({
message: 'Hello from main thread',
config: { timeout: 5000, retries: 3 },
users: ['alice', 'bob', 'charlie']
});

const data = inject();

// Verify all property types work correctly
assert.strictEqual(data.message, 'Hello from main thread', 'String property should work');
assert.strictEqual(data.config.timeout, 5000, 'Nested object property should work');
assert.strictEqual(data.config.retries, 3, 'Nested object property should work');
assert(Array.isArray(data.users), 'Array property should work');
assert.strictEqual(data.users.length, 3, 'Array length should work');
assert.strictEqual(data.users[0], 'alice', 'Array access should work');

// Test 2: Data overriding
share({ message: 'Updated message' });
const updatedData = inject();
assert.strictEqual(updatedData.message, 'Updated message', 'Data override should work');
assert.strictEqual(updatedData.config.timeout, 5000, 'Previous data should persist');

// Test 3: Complex nested structures
share({
testSuite: {
name: 'E2E Tests',
tests: [
{ name: 'Login test', status: 'passed', data: { user: 'admin', pass: 'secret' } },
{ name: 'Checkout test', status: 'failed', error: 'Timeout occurred' }
],
metadata: {
browser: 'chrome',
version: '91.0',
viewport: { width: 1920, height: 1080 }
}
}
});

const complexData = inject();
assert.strictEqual(complexData.testSuite.name, 'E2E Tests', 'Deep nested string should work');
assert.strictEqual(complexData.testSuite.tests[0].data.user, 'admin', 'Very deep nested access should work');
assert.strictEqual(complexData.testSuite.metadata.viewport.width, 1920, 'Very deep nested number should work');

// Test 4: Key enumeration
const allKeys = Object.keys(inject());
assert(allKeys.includes('message'), 'Keys should include shared properties');
assert(allKeys.includes('testSuite'), 'Keys should include all shared properties');

console.log('✅ ALL TESTS PASSED - Issue #5066 is completely fixed!');
console.log('✅ Workers can now share and inject data without circular dependency errors');
console.log('✅ Proxy objects work correctly for both direct and nested property access');
});
42 changes: 42 additions & 0 deletions test/data/sandbox/workers-proxy-issue/proxy_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const assert = require('assert');

Feature('Fix for issue #5066: Unable to inject data between workers because of proxy object');

Scenario('Basic share and inject functionality', () => {
console.log('Testing basic share() and inject() functionality...');

// This is the basic pattern that should work after the fix
const originalData = { message: 'Hello', count: 42 };
share(originalData);

const injectedData = inject();
console.log('Shared data keys:', Object.keys(originalData));
console.log('Injected data keys:', Object.keys(injectedData));

// These assertions should pass after the fix
assert.strictEqual(injectedData.message, 'Hello', 'String property should be accessible');
assert.strictEqual(injectedData.count, 42, 'Number property should be accessible');

console.log('✅ SUCCESS: Basic share/inject works!');
});

Scenario('Complex nested data structures', () => {
console.log('Testing complex nested data sharing...');

const testDataJson = {
users: [{ id: 1, name: 'John' }, { id: 2, name: 'Jane' }],
settings: { theme: 'dark', language: 'en' }
};

share({ testDataJson });

const data = inject();

// These should work after the fix
assert(data.testDataJson, 'testDataJson should be accessible');
assert(Array.isArray(data.testDataJson.users), 'users should be an array');
assert.strictEqual(data.testDataJson.users[0].name, 'John', 'Should access nested user data');
assert.strictEqual(data.testDataJson.settings.theme, 'dark', 'Should access nested settings');

console.log('✅ SUCCESS: Complex nested data works!');
});
35 changes: 35 additions & 0 deletions test/unit/workerStorage_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const { expect } = require('expect');
const WorkerStorage = require('../../lib/workerStorage');
const { Worker } = require('worker_threads');
const event = require('../../lib/event');

describe('WorkerStorage', () => {
it('should handle share message correctly without circular dependency', (done) => {
// Create a mock worker to test the functionality
const mockWorker = {
threadId: 'test-thread-1',
on: (eventName, callback) => {
if (eventName === 'message') {
// Simulate receiving a share message
setTimeout(() => {
callback({ event: 'share', data: { testKey: 'testValue' } });
done();
}, 10);
}
},
postMessage: () => {}
};

// Add the mock worker to storage
WorkerStorage.addWorker(mockWorker);
});

it('should not crash when sharing data', () => {
const testData = { user: 'test', password: '123' };

// This should not throw an error
expect(() => {
WorkerStorage.share(testData);
}).not.toThrow();
});
});
Loading