Skip to content
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

xpath: support for indexed-repeat function #195

Merged
merged 16 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6e2cf9d
Move already ported JR tests exercising `indexed-repeat` to dedicated…
eyelidlessness Aug 20, 2024
aad995b
Update ported JR `indexed-repeat` tests for consistent XPath referenc…
eyelidlessness Aug 20, 2024
fc30485
Add explicit repeat creation in ported JR indexed-repeat tests
eyelidlessness Aug 21, 2024
59ccb8a
Port tests from JR 776 draft PR extending `indexed-repeat` coverage
eyelidlessness Aug 20, 2024
541cd12
Add tests for indexed-repeat with nested repeats (up to depth 3), arg…
eyelidlessness Aug 22, 2024
f01973e
`indexed-repeat` function implementation
eyelidlessness Aug 22, 2024
dddbae3
Update `indexed-repeat` Scenario tests to reflect those now passing…
eyelidlessness Aug 22, 2024
fcb402f
Update child-vaccination smoketest accounting for progress to new kno…
eyelidlessness Aug 22, 2024
b3b803c
chore: xpath package consistent test suffixes…
eyelidlessness Jun 26, 2024
00ed5f3
xpath: indexed-repeat unit-ish level tests (part 1)
eyelidlessness Aug 22, 2024
8ed21a7
xpath indexed-repeat tests: depth 1, target arg is relative to contex…
eyelidlessness Aug 22, 2024
8f24309
xpath indexed-repeat tests: depth 2, target/repeat2 args relative to …
eyelidlessness Aug 22, 2024
7172db0
xpath indexed-repeat tests: depth 3, remainder of cases relative to c…
eyelidlessness Aug 22, 2024
746aa8a
Changeset
eyelidlessness Aug 22, 2024
7742cb9
Clarify when containment filtering is applicable
eyelidlessness Aug 27, 2024
bbbd2a2
scenario: add indexed-repeat tests specifically exercising node-set b…
eyelidlessness Aug 28, 2024
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
6 changes: 6 additions & 0 deletions .changeset/rotten-turtles-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@getodk/scenario': minor
'@getodk/xpath': minor
---

Support for `indexed-repeat` XPath function
11 changes: 7 additions & 4 deletions packages/scenario/src/jr/Scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ export class Scenario {
readonly formName: string;
readonly instanceRoot: RootNode;

private readonly getPositionalEvents: Accessor<PositionalEvents>;
protected readonly getPositionalEvents: Accessor<PositionalEvents>;

protected readonly getEventPosition: Accessor<number>;
private readonly setEventPosition: Setter<number>;

protected readonly getSelectedPositionalEvent: Accessor<AnyPositionalEvent>;
Expand All @@ -154,14 +156,15 @@ export class Scenario {
this.formName = formName;
this.instanceRoot = instanceRoot;

const [eventPosition, setEventPosition] = createSignal(0);
const [getEventPosition, setEventPosition] = createSignal(0);

this.getPositionalEvents = () => getPositionalEvents(instanceRoot);
this.getEventPosition = getEventPosition;
this.setEventPosition = setEventPosition;

this.getSelectedPositionalEvent = createMemo(() => {
const events = getPositionalEvents(instanceRoot);
const position = eventPosition();
const position = getEventPosition();
const event = events[position];

if (event == null) {
Expand Down Expand Up @@ -386,7 +389,7 @@ export class Scenario {
const node = getNodeForReference(this.instanceRoot, reference);

if (node == null) {
throw new Error(`No "answer" node for reference: ${reference}`);
throw new Error(`No instance node for reference: ${reference}`);
}

return node;
Expand Down
127 changes: 0 additions & 127 deletions packages/scenario/test/repeat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2068,133 +2068,6 @@ describe('Tests ported from JavaRosa - repeats', () => {
});
});

describe('IndexedRepeatRelativeRefsTest.java', () => {
const ABSOLUTE_TARGET = '/data/some-group/item/value';
const RELATIVE_TARGET = '../item/value';
const ABSOLUTE_GROUP = '/data/some-group/item';
const RELATIVE_GROUP = '../item';
const ABSOLUTE_INDEX = '/data/total-items';
const RELATIVE_INDEX = '../../total-items';

interface IndexedRepeatRelativeRefsOptions {
readonly testName: string;
readonly target: string;
readonly group: string;
readonly index: string;
}

const parameters: readonly IndexedRepeatRelativeRefsOptions[] = [
{
testName: 'Target: absolute, group: absolute, index: absolute',
target: ABSOLUTE_TARGET,
group: ABSOLUTE_GROUP,
index: ABSOLUTE_INDEX,
},
{
testName: 'Target: absolute, group: absolute, index: relative',
target: ABSOLUTE_TARGET,
group: ABSOLUTE_GROUP,
index: RELATIVE_INDEX,
},
{
testName: 'Target: absolute, group: relative, index: absolute',
target: ABSOLUTE_TARGET,
group: RELATIVE_GROUP,
index: ABSOLUTE_INDEX,
},
{
testName: 'Target: absolute, group: relative, index: relative',
target: ABSOLUTE_TARGET,
group: RELATIVE_GROUP,
index: RELATIVE_INDEX,
},
{
testName: 'Target: relative, group: absolute, index: absolute',
target: RELATIVE_TARGET,
group: ABSOLUTE_GROUP,
index: ABSOLUTE_INDEX,
},
{
testName: 'Target: relative, group: absolute, index: relative',
target: RELATIVE_TARGET,
group: ABSOLUTE_GROUP,
index: RELATIVE_INDEX,
},
{
testName: 'Target: relative, group: relative, index: absolute',
target: RELATIVE_TARGET,
group: RELATIVE_GROUP,
index: ABSOLUTE_INDEX,
},
{
testName: 'Target: relative, group: relative, index: relative',
target: RELATIVE_TARGET,
group: RELATIVE_GROUP,
index: RELATIVE_INDEX,
},
];

/**
* **PORTING NOTES**
*
* - Fails pending implementation of `indexed-repeat` XPath function.
*
* - Parameters adapted to match values in JavaRosa. Note that the
* parameters are passed as {@link options} rather than destructured. Java
* lets you reference `group` (the class property) and `group` (the
* imported static method) in the same scope. TypeScript/JavaScript don't
* let you do that... which is fine, because doing that is really weird!
*/
it.fails.each<IndexedRepeatRelativeRefsOptions>(parameters)('$testName', async (options) => {
const scenario = await Scenario.init(
'Some form',
html(
head(
title('Some form'),
model(
mainInstance(
t(
'data id="some-form"',
t('some-group', t('item jr:template=""', t('value')), t('last-value')),
t('total-items')
)
),
bind(ABSOLUTE_TARGET).type('int'),
bind('/data/total-items').type('int').calculate('count(/data/some-group/item)'),
bind('/data/some-group/last-value')
.type('int')
.calculate(
'indexed-repeat(' +
options.target +
', ' +
options.group +
', ' +
options.index +
')'
)
)
),
body(
group(
'/data/some-group',
group(
'/data/some-group/item',
repeat('/data/some-group/item', input('/data/some-group/item/value'))
)
)
)
)
);

scenario.answer('/data/some-group[1]/item[1]/value', 11);
scenario.answer('/data/some-group[1]/item[2]/value', 22);
scenario.answer('/data/some-group[1]/item[3]/value', 33);

expect(scenario.answerOf('/data/total-items')).toEqualAnswer(intAnswer(3));
expect(scenario.answerOf('/data/some-group/last-value')).toEqualAnswer(intAnswer(33));
});
});

describe('TriggersForRelativeRefsTest.java (regression tests)', () => {
describe('indefinite repeat `jr:count` expression', () => {
/**
Expand Down
112 changes: 70 additions & 42 deletions packages/scenario/test/smoketests/child-vaccination.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ import type {
} from '../../src/jr/event/getPositionalEvents.ts';
import { JRTreeReference as BaseJRTreeReference } from '../../src/jr/xpath/JRTreeReference.ts';

/**
* Intentionally naive. The scope of expressions we expect to handle in this
* test suite is narrow enough that we don't need to do more robust static
* analysis and serialization. There is more complete logic in
* `@getodk/xforms-engine` to do this, but it's intentionally project-private
* (at least it is at time of writing).
*/
const naiveStripPositionalPredicates = (expression: string): string => {
return expression.replaceAll(/\[\d+\]/g, '');
};

/**
* **PORTING NOTES**
*
Expand Down Expand Up @@ -54,7 +65,9 @@ const refSingletons = new UpsertableMap<string, JRTreeReference>();

class JRTreeReference extends BaseJRTreeReference {
genericize(): JRTreeReference {
throw new IncompleteTestPortError('TreeReference.genericize');
const reference = naiveStripPositionalPredicates(this.xpathReference);

return new JRTreeReference(reference);
}

equals(other: JRTreeReference): boolean {
Expand Down Expand Up @@ -123,6 +136,24 @@ class Scenario extends BaseScenario {
/* eslint-enable no-console */
}

private getNextEventPosition(): AnyPositionalEvent {
const currentPosition = this.getSelectedPositionalEvent();

if (currentPosition.eventType === 'END_OF_FORM') {
throw 'todo';
}

const events = this.getPositionalEvents();
const position = this.getEventPosition();
const next = events[position + 1];

if (next == null) {
throw new Error(`No question at position: ${position}`);
}

return next;
}

/**
* @deprecated
*
Expand All @@ -142,7 +173,13 @@ class Scenario extends BaseScenario {
* increased clarity of intent.
*/
nextRef(): JRTreeReference {
throw new IncompleteTestPortError('Extended Scenario.nextRef');
const next = this.getNextEventPosition();

if (next.eventType === 'END_OF_FORM') {
throw 'todo';
}

return new JRTreeReference(next.node.currentState.reference);
}

/**
Expand Down Expand Up @@ -549,42 +586,32 @@ describe('ChildVaccinationTest.java', () => {
});

it.fails('[smoke test]', async () => {
let scenario: Scenario | null = null;
const scenario = await Scenario.init('child_vaccination_VOL_tool_v12.xml');

try {
scenario = await Scenario.init('child_vaccination_VOL_tool_v12.xml');

expect.fail('Update `child-vaccination.test.ts`, known failure mode has changed');
} catch (error) {
expect(error).toBeInstanceOf(Error);
scenario.next('/data/building_type');
scenario.answer('multi');
scenario.next('/data/not_single');
scenario.next('/data/not_single/gps');
scenario.answer('1.234 5.678');
scenario.next('/data/building_name');
scenario.answer('Some building');
scenario.next('/data/full_address1');
scenario.answer('Some address, some location');

// Failure of this assertion likely means that we've implemented the
// `indexed-repeat` XPath function. When that occurs, these error
// condition assertions should be removed, and the `Scenario.init` call
// should be treated normally.
//
// If a new failure occurs after that point, and that failure cannot be
// addressed by updating the test to be more complete (e.g. by specifying
// more of the expected references in `scenario.next` calls, or
// implementing other aspects of the test which are currently deferred),
// consider adding a similar function/assertion/comment to this effect,
// asserting that new known failure condition and prompting the test to be
// updated again once it is resolved.
expect((error as Error).message).toContain('function not defined: indexed-repeat');
}
// endregion

if (scenario == null) {
return;
}
const currentExpectedPointOfFailure = () => {
expect(scenario.answerOf('/data/household_count').toString()).toBe('2');

scenario.next('/data/building_type');
const secondHouseholdControlledByRepeatCount = scenario.getInstanceNode('/data/household[2]');

const currentExpectedPointOfFailure = () => {
scenario.answer('multi');
expect(secondHouseholdControlledByRepeatCount).not.toBeNull();
};

try {
expect(currentExpectedPointOfFailure).toThrowError('function not defined: indexed-repeat');
expect(currentExpectedPointOfFailure).toThrowError(
'No instance node for reference: /data/household[2]'
);

if (typeof currentExpectedPointOfFailure === 'function') {
scenario.trace(
Expand All @@ -593,20 +620,21 @@ describe('ChildVaccinationTest.java', () => {

return;
}
} catch {
throw new Error();
}
} catch (error) {
// Failure of this assertion likely means that we've implemented
// `jr:count`. When that occurs, these error condition assertions should
// be removed, and the test should be updated to add expected node-set
// reference assertions in the `scenario.next` calls, and so on, either
// until the test passes or a new known point of failure is identified.

scenario.answer('multi');
scenario.next('/data/not_single');
scenario.next('/data/not_single/gps');
scenario.answer('1.234 5.678');
scenario.next('/data/building_name');
scenario.answer('Some building');
scenario.next('/data/full_address1');
scenario.answer('Some address, some location');
expect(error).toBeInstanceOf(Error);

// endregion
const { message } = error as Error;

expect.fail(
`Update \`child-vaccination.test.ts\`, known failure mode has changed: ${message}`
);
}

// region Answer all household repeats

Expand Down
Loading