-
Notifications
You must be signed in to change notification settings - Fork 91
fix(ethercat): prevent slave name collisions across masters [DOPE-281] #754
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
60b71d6
fix(ethercat): prevent slave name collisions across masters [DOPE-281]
marconetsf 5fd8358
Merge branch 'development' into fix/ethercat-slave-name-collision
marconetsf 018f7fe
fix(ethercat): move slave-name dedup helper to frontend/utils [DOPE-281]
marconetsf 189c134
style(ethercat): apply prettier to unique-slave-name test
marconetsf 7679a9f
fix(ethercat): harden short device name fallback [DOPE-281]
marconetsf ac8d44a
Merge branch 'development' into fix/ethercat-slave-name-collision
marconetsf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| import { getShortDeviceName } from '../short-device-name' | ||
|
|
||
| const make = ( | ||
| overrides: Partial<{ typeName: string; name: string; productCode: string; revisionNo: string }> = {}, | ||
| ) => ({ | ||
| type: { | ||
| name: overrides.typeName ?? '', | ||
| productCode: overrides.productCode ?? '0x07212C52', | ||
| revisionNo: overrides.revisionNo ?? '0x00110000', | ||
| }, | ||
| name: overrides.name ?? '', | ||
| }) | ||
|
|
||
| describe('getShortDeviceName', () => { | ||
| describe('P1: <Type> text as short code', () => { | ||
| it('returns <Type> text when it looks like an SKU', () => { | ||
| expect(getShortDeviceName(make({ typeName: 'EL1809' }))).toBe('EL1809') | ||
| }) | ||
|
|
||
| it('accepts SKUs with hyphens up to 24 chars', () => { | ||
| expect(getShortDeviceName(make({ typeName: 'EL2521-0124-0010' }))).toBe('EL2521-0124-0010') | ||
| }) | ||
|
|
||
| it('trims surrounding whitespace before evaluating', () => { | ||
| expect(getShortDeviceName(make({ typeName: ' EL1809 ' }))).toBe('EL1809') | ||
| }) | ||
|
|
||
| it('falls through to P2 when <Type> text contains internal whitespace', () => { | ||
| // P1 rejects (whitespace), P2 takes first token "EK1100" — SKU-shaped, returned | ||
| expect(getShortDeviceName(make({ typeName: 'Generic Coupler', name: 'EK1100 EtherCAT Coupler' }))).toBe('EK1100') | ||
| }) | ||
|
|
||
| it('falls through to P3 when both P1 and P2 reject but <Type> text exists', () => { | ||
| // P1 rejects (whitespace), P2 rejects ("Generic" has no digit), P3 returns <Type> text | ||
| expect(getShortDeviceName(make({ typeName: 'Generic Coupler', name: 'Generic Coupler description' }))).toBe( | ||
| 'Generic Coupler', | ||
| ) | ||
| }) | ||
|
|
||
| it('falls through when <Type> text is longer than 24 chars', () => { | ||
| expect( | ||
| getShortDeviceName(make({ typeName: 'ExtraLongDescriptiveTypeName123', name: 'EL1809 2Ch. Digital Input' })), | ||
| ).toBe('EL1809') | ||
| }) | ||
| }) | ||
|
|
||
| describe('P2: first token of <Name> as SKU', () => { | ||
| it('extracts SKU when it leads the long name', () => { | ||
| expect(getShortDeviceName(make({ name: 'EL1809 2Ch. Digital Input 24V, 3ms' }))).toBe('EL1809') | ||
| }) | ||
|
|
||
| it('handles comma and semicolon separators', () => { | ||
| expect(getShortDeviceName(make({ name: 'EK1100,EtherCAT Coupler' }))).toBe('EK1100') | ||
| }) | ||
|
|
||
| it('rejects digit-leading tokens like "2-Channel"', () => { | ||
| // P2 rejects, P3 unavailable, P4 returns the long name as-is | ||
| expect(getShortDeviceName(make({ name: '2-Channel Digital Input' }))).toBe('2-Channel Digital Input') | ||
| }) | ||
|
|
||
| it('rejects pure-letter tokens like "EtherCAT"', () => { | ||
| expect(getShortDeviceName(make({ name: 'EtherCAT Generic Slave' }))).toBe('EtherCAT Generic Slave') | ||
| }) | ||
|
|
||
| it('rejects tokens shorter than 3 chars even if SKU-shaped', () => { | ||
| expect(getShortDeviceName(make({ name: 'A1 short token here' }))).toBe('A1 short token here') | ||
| }) | ||
|
|
||
| it('rejects tokens longer than 24 chars', () => { | ||
| const longToken = 'X' + '1'.repeat(24) | ||
| expect(getShortDeviceName(make({ name: `${longToken} description` }))).toBe(`${longToken} description`) | ||
| }) | ||
|
|
||
| it('accepts SKUs with mixed digits and hyphens (e.g. Omron R88D-1SN02H-ECT)', () => { | ||
| expect(getShortDeviceName(make({ name: 'R88D-1SN02H-ECT Servo Drive' }))).toBe('R88D-1SN02H-ECT') | ||
| }) | ||
| }) | ||
|
|
||
| describe('P3: <Type> text as last readable fallback', () => { | ||
| it('returns <Type> text when it has whitespace and P2 finds nothing usable', () => { | ||
| expect(getShortDeviceName(make({ typeName: 'Generic Coupler' }))).toBe('Generic Coupler') | ||
| }) | ||
| }) | ||
|
|
||
| describe('P4: long name as-is', () => { | ||
| it('returns the long name when both P1 and P2 reject', () => { | ||
| expect(getShortDeviceName(make({ name: 'Generic EtherCAT Slave' }))).toBe('Generic EtherCAT Slave') | ||
| }) | ||
| }) | ||
|
|
||
| describe('P5: canonical identity fallback', () => { | ||
| it('returns Device_{productCode}_{revisionNo} when no name is available', () => { | ||
| expect(getShortDeviceName(make({ productCode: '0x07212C52', revisionNo: '0x00110000' }))).toBe( | ||
| 'Device_0x07212C52_0x00110000', | ||
| ) | ||
| }) | ||
|
|
||
| it('treats whitespace-only names as empty', () => { | ||
| expect(getShortDeviceName(make({ typeName: ' ', name: ' ' }))).toBe('Device_0x07212C52_0x00110000') | ||
| }) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { collectAllSlaveNames, generateUniqueSlaveName } from '../unique-slave-name' | ||
|
|
||
| describe('collectAllSlaveNames', () => { | ||
| it('returns empty set when remoteDevices is undefined', () => { | ||
| expect(collectAllSlaveNames(undefined)).toEqual(new Set()) | ||
| }) | ||
|
|
||
| it('returns empty set when there are no remote devices', () => { | ||
| expect(collectAllSlaveNames([])).toEqual(new Set()) | ||
| }) | ||
|
|
||
| it('returns empty set when remote devices have no ethercat config', () => { | ||
| expect(collectAllSlaveNames([{}, { ethercatConfig: undefined }])).toEqual(new Set()) | ||
| }) | ||
|
|
||
| it('returns empty set when ethercat config has no devices array', () => { | ||
| expect(collectAllSlaveNames([{ ethercatConfig: {} }])).toEqual(new Set()) | ||
| }) | ||
|
|
||
| it('collects names from a single master', () => { | ||
| const result = collectAllSlaveNames([{ ethercatConfig: { devices: [{ name: 'EL1809' }, { name: 'EL2008' }] } }]) | ||
| expect(result).toEqual(new Set(['EL1809', 'EL2008'])) | ||
| }) | ||
|
|
||
| it('collects names across multiple masters and deduplicates', () => { | ||
| const result = collectAllSlaveNames([ | ||
| { ethercatConfig: { devices: [{ name: 'EL1809' }, { name: 'EL2008' }] } }, | ||
| { ethercatConfig: { devices: [{ name: 'EL1809' }, { name: 'EL3104' }] } }, | ||
| ]) | ||
| expect(result).toEqual(new Set(['EL1809', 'EL2008', 'EL3104'])) | ||
| }) | ||
| }) | ||
|
|
||
| describe('generateUniqueSlaveName', () => { | ||
| it('returns base when not taken', () => { | ||
| expect(generateUniqueSlaveName('EL1809', [])).toBe('EL1809') | ||
| expect(generateUniqueSlaveName('EL1809', ['EL2008'])).toBe('EL1809') | ||
| }) | ||
|
|
||
| it('returns _01 suffix on first collision', () => { | ||
| expect(generateUniqueSlaveName('EL1809', ['EL1809'])).toBe('EL1809_01') | ||
| }) | ||
|
|
||
| it('skips taken suffixes and picks the next free one', () => { | ||
| expect(generateUniqueSlaveName('EL1809', ['EL1809', 'EL1809_01', 'EL1809_02'])).toBe('EL1809_03') | ||
| }) | ||
|
|
||
| it('pads single digits to two digits and widens past 99', () => { | ||
| const taken = new Set<string>(['EL1809']) | ||
| for (let i = 1; i <= 99; i++) taken.add(`EL1809_${String(i).padStart(2, '0')}`) | ||
| expect(generateUniqueSlaveName('EL1809', taken)).toBe('EL1809_100') | ||
| }) | ||
|
|
||
| it('accepts a Set directly as the existing argument', () => { | ||
| expect(generateUniqueSlaveName('EL1809', new Set(['EL1809']))).toBe('EL1809_01') | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.