diff --git a/client/src/webview/script.spec.ts b/client/src/webview/script.spec.ts index dbd0e44..1a35885 100644 --- a/client/src/webview/script.spec.ts +++ b/client/src/webview/script.spec.ts @@ -370,12 +370,12 @@ describe('webview script', () => { stateError('Second state error', { ...errorMachine('Second', 'reset'), errorContext: null }, 9), ]); click('[data-tab="fsm"]'); - expect(root.querySelector('.diagram-error-text')?.textContent).toBe('The call at line 5 is rejected'); + expect(root.querySelector('.diagram-error-text')?.textContent).toBe('Invalid method call at line 5'); expect(root.querySelector('.state-error-position')?.textContent).toBe('1 of 2'); click('.state-error-step-btn[data-step="1"]'); expect(root.querySelector('.diagram-title')?.textContent).toBe('Second'); - expect(root.querySelector('.diagram-error-text')?.textContent).toBe('The call at line 10 is rejected'); + expect(root.querySelector('.diagram-error-text')?.textContent).toBe('Invalid method call at line 10'); click('#state-error-reveal-btn'); expect(root.querySelector('.nav-tab.selected')?.getAttribute('data-tab')).toBe('diagnostics'); diff --git a/client/src/webview/views/fsm/fsm.spec.ts b/client/src/webview/views/fsm/fsm.spec.ts index aaab5c9..455acae 100644 --- a/client/src/webview/views/fsm/fsm.spec.ts +++ b/client/src/webview/views/fsm/fsm.spec.ts @@ -119,7 +119,7 @@ describe('state machine view', () => { } }); - it('summarizes where the call is rejected and allowed in one line', () => { + it('summarizes the invalid call with the expected and actual states in one line', () => { const errorMachine = machine({ states: ['unbound', 'bound', 'connected', 'closed'], errorContext: socketError(['bound', 'closed']), @@ -127,15 +127,15 @@ describe('state machine view', () => { renderStateMachineView(document.body, [errorMachine], [diagram], 'TB', false, true); const summary = document.querySelector('.diagram-error-text')!; - expect(summary.textContent).toBe('socket.receive() is rejected when socket is in state closed \u00B7 allowed in state bound'); - expect(Array.from(summary.querySelectorAll('code'), node => node.textContent)).toEqual(['socket.receive()', 'socket', 'closed', 'bound']); + expect(summary.textContent).toBe('Invalid method call socket.receive() \u00B7 expected state unbound or bound or connected \u00B7 actual state bound or closed'); + expect(Array.from(summary.querySelectorAll('code'), node => node.textContent)).toEqual(['socket.receive()', 'unbound', 'bound', 'connected', 'bound', 'closed']); expect(button('diagram-focus-btn').textContent?.trim()).toBe('Show all'); renderStateMachineView(document.body, [errorMachine], [diagram], 'TB', false, false); expect(button('diagram-focus-btn').textContent?.trim()).toBe('Focus on error'); }); - it('omits the allowed part of the summary when no possible state accepts the call', () => { + it('names the expected and actual states when no actual is expected', () => { renderStateMachineView(document.body, [machine({ states: ['childrenAllowed', 'maybeHasChildren'], errorContext: { @@ -144,19 +144,19 @@ describe('state machine view', () => { }, })], [diagram], 'TB', false, true); - expect(document.querySelector('.diagram-error-text')?.textContent).toBe('root.getFirstChild() is rejected when root is in state childrenAllowed'); + expect(document.querySelector('.diagram-error-text')?.textContent).toBe('Invalid method call root.getFirstChild() \u00B7 expected state maybeHasChildren \u00B7 actual state childrenAllowed'); }); it.each([ [ 'the state of the receiver is unknown', ['unbound', 'bound', 'connected', 'closed'], - 'socket.receive() is rejected: the state of socket is unknown here \u00B7 allowed in state unbound or bound or connected', + 'Invalid method call socket.receive() \u00B7 expected state unbound or bound or connected \u00B7 actual state unknown', ], [ - 'every possible state accepts the call', + 'every actual is expected', ['bound'], - 'socket.receive() is rejected here \u00B7 allowed in state bound', + 'Invalid method call socket.receive() \u00B7 expected state unbound or bound or connected \u00B7 actual state bound', ], ])('summarizes the error when %s', (_, possibleStates, summary) => { renderStateMachineView(document.body, [machine({ @@ -168,9 +168,9 @@ describe('state machine view', () => { }); it.each([ - ['invalid, valid, and other accepted states', ['bound', 'closed'], ['\u2715 Call rejected', '\u2713 Call allowed', 'Allowed, but not reachable here']], - ['only an invalid and other accepted states', ['closed'], ['\u2715 Call rejected', 'Allowed, but not reachable here']], - ['an unknown receiver state', ['unbound', 'bound', 'connected', 'closed'], ['Allowed, but not reachable here']], + ['invalid, valid, and other accepted states', ['bound', 'closed'], ['\u2715 Actual state, not expected', '\u2713 Actual state, expected', 'Expected state']], + ['only an invalid and other accepted states', ['closed'], ['\u2715 Actual state, not expected', 'Expected state']], + ['an unknown receiver state', ['unbound', 'bound', 'connected', 'closed'], ['Expected state']], ])('explains the state marks in a legend with %s', (_, possibleStates, items) => { renderStateMachineView(document.body, [machine({ states: ['unbound', 'bound', 'connected', 'closed'], @@ -200,8 +200,8 @@ describe('state machine view', () => { ['without the accepted states', { calledMethod: 'open', receiver: 'counter', possibleStates: ['Closed'], acceptedStates: [], validStates: [], invalidStates: [], - }, 'counter.open() is rejected here'], - ['without an error context', null, 'The call at line 12 is rejected'], + }, 'Invalid method call counter.open()'], + ['without an error context', null, 'Invalid method call at line 12'], ])('keeps the stepper and View error for a shown error %s', (_, errorContext, text) => { renderStateMachineView(document.body, [machine({ errorContext })], [diagram], 'TB', false, true, { index: 1, count: 2, line: 12 }); @@ -213,10 +213,10 @@ describe('state machine view', () => { expect(document.querySelector('#diagram-focus-btn')).toBeNull(); }); - it('says the call is rejected without a line when the shown error has no position', () => { + it('names the invalid call without a line when the shown error has no position', () => { renderStateMachineView(document.body, [machine()], [diagram], 'TB', false, true, { index: -1, count: 0, line: null }); - expect(document.querySelector('.diagram-error-text')?.textContent).toBe('The call is rejected'); + expect(document.querySelector('.diagram-error-text')?.textContent).toBe('Invalid method call'); }); it('clips the panned diagram below the error summary and legend', () => { @@ -338,7 +338,7 @@ describe('state machine view', () => { expect(Array.from(document.querySelectorAll('.diagram-state-set'), node => node.classList.contains('diagram-state-set-dimmed'))) .toEqual([false, true]); expect(document.querySelector('.diagram-error-text')?.textContent) - .toBe('param.setCompressionType() is rejected when param is in state startCompression'); + .toBe('Invalid method call param.setCompressionType() \u00B7 expected state compressionExplicit \u00B7 actual state startCompression'); renderStateMachineView(document.body, sms, ['compression diagram', 'tiling diagram'], 'TB', false, false); expect(document.querySelector('.diagram-state-set-dimmed')).toBeNull(); @@ -354,10 +354,10 @@ describe('state machine view', () => { renderStateMachineView(document.body, sms, ['tiling diagram', 'compression diagram'], 'TB', false, true); expect(document.querySelector('.diagram-error-text')?.textContent) - .toBe('param.write() is rejected when param is in state startTiling \u00B7 allowed in state compressionExplicit'); + .toBe('Invalid method call param.write() \u00B7 expected state tilingSet and compressionExplicit \u00B7 actual state startTiling and compressionExplicit'); expect(document.querySelector('.diagram-state-set-dimmed')).toBeNull(); expect(Array.from(document.querySelectorAll('.diagram-error-legend-item'), node => node.textContent)) - .toEqual(['\u2715 Call rejected', '\u2713 Call allowed', 'Allowed, but not reachable here']); + .toEqual(['\u2715 Actual state, not expected', '\u2713 Actual state, expected', 'Expected state']); }); it('names the call of the state set with the error when it cannot be marked', () => { @@ -365,7 +365,7 @@ describe('state machine view', () => { renderStateMachineView(document.body, [{ ...compression, errorContext: unmarked }, tiling], ['compression diagram', 'tiling diagram'], 'TB', false, true, { index: 0, count: 1, line: 7 }); - expect(document.querySelector('.diagram-error-text')?.textContent).toBe('param.setCompressionType() is rejected here'); + expect(document.querySelector('.diagram-error-text')?.textContent).toBe('Invalid method call param.setCompressionType()'); // nothing is marked, so nothing is dimmed expect(document.querySelector('.diagram-state-set-dimmed')).toBeNull(); }); diff --git a/client/src/webview/views/fsm/fsm.ts b/client/src/webview/views/fsm/fsm.ts index 289da27..5e08ae7 100644 --- a/client/src/webview/views/fsm/fsm.ts +++ b/client/src/webview/views/fsm/fsm.ts @@ -93,39 +93,27 @@ function isStateErrorShown(stateErrors: StateErrorNavigation): boolean { return stateErrors.index >= 0 || stateErrors.line !== undefined; } -// summarises the error of every state set it involves, which is rejected when the receiver is in any invalid state +// summarises the error of every state set it involves as a compiler would: the invalid call, then the expected and actual states function getStateErrorSummary(focuses: StateErrorFocus[]): string { - const focus = focuses[0]; - const call = getRejectedCallText(focus); - const receiver = focus.receiver ? `${escapeHtml(focus.receiver)}` : 'the receiver'; - const rejected = `${call ? `${escapeHtml(call)}` : 'The call'} is rejected`; - const invalidStates = focuses.flatMap(focus => focus.invalidStates); - const allowedStates = focuses - .map(focus => focus.unknownState ? focus.acceptedStates : focus.validStates) - .filter(states => states.length > 0); - let summary: string; - if (invalidStates.length > 0) { - summary = `${rejected} when ${receiver} is in state ${renderStates(invalidStates)}`; - } else if (focuses.every(focus => focus.unknownState)) { - summary = `${rejected}: the state of ${receiver} is unknown here`; - } else { - summary = `${rejected} here`; - } - // the call needs an allowed state of each state set - if (allowedStates.length > 0) { - summary += ` \u00B7 allowed in state ${allowedStates - .map(states => allowedStates.length > 1 && states.length > 1 ? `(${renderStates(states)})` : renderStates(states)) - .join(' and ')}`; - } - return summary; + const call = getRejectedCallText(focuses[0]); + // the call needs an expected state of each state set + const expected = renderStateSets(focuses.map(focus => focus.acceptedStates)); + const actual = focuses.every(focus => focus.unknownState) + ? 'unknown' + : renderStateSets(focuses.filter(focus => !focus.unknownState).map(focus => focus.possibleStates)); + return `${renderInvalidCall(call)} \u00B7 expected state ${expected} \u00B7 actual state ${actual}`; } // the shown error cannot be marked on the diagrams when the verifier could not tell which states allow the call function getUnmarkedStateErrorSummary(sms: LJStateMachine[], stateErrors: StateErrorNavigation): string { const context = sms.find(sm => sm.errorContext)?.errorContext; const call = context ? getRejectedCallText(context) : null; - if (call) return `${escapeHtml(call)} is rejected here`; - return stateErrors.line ? `The call at line ${stateErrors.line} is rejected` : 'The call is rejected'; + if (call || !stateErrors.line) return renderInvalidCall(call); + return `Invalid method call at line ${stateErrors.line}`; +} + +function renderInvalidCall(call: string | null): string { + return call ? `Invalid method call ${escapeHtml(call)}` : 'Invalid method call'; } function renderStateErrorSummary(summary: string, stateErrors: StateErrorNavigation): string { @@ -153,10 +141,10 @@ function renderStateErrorStepper({ index, count }: StateErrorNavigation): string // explains the state marks with the same outlines, fills, and icons as the diagram, next to the toggle that dims the rest function renderStateErrorLegend(focuses: StateErrorFocus[], focusOnError: boolean): string { const items = [ - focuses.some(focus => focus.invalidStates.length > 0) ? renderLegendItem('invalid', `${INVALID_STATE_ICON} Call rejected`) : '', - focuses.some(focus => focus.validStates.length > 0) ? renderLegendItem('valid', `${VALID_STATE_ICON} Call allowed`) : '', + focuses.some(focus => focus.invalidStates.length > 0) ? renderLegendItem('invalid', `${INVALID_STATE_ICON} Actual state, not expected`) : '', + focuses.some(focus => focus.validStates.length > 0) ? renderLegendItem('valid', `${VALID_STATE_ICON} Actual state, expected`) : '', focuses.some(focus => focus.acceptedStates.some(state => !focus.validStates.includes(state))) - ? renderLegendItem('accepted', 'Allowed, but not reachable here') + ? renderLegendItem('accepted', 'Expected state') : '', ].filter(Boolean); return /*html*/` @@ -173,6 +161,13 @@ function renderLegendItem(kind: string, label: string): string { return `${label}`; } +// the states of each state set, the call needs one state of each +function renderStateSets(stateSets: string[][]): string { + return stateSets + .map(states => stateSets.length > 1 && states.length > 1 ? `(${renderStates(states)})` : renderStates(states)) + .join(' and '); +} + function renderStates(states: string[]): string { return states.map(state => `${escapeHtml(state)}`).join(' or '); }