Skip to content

Commit 1091ec8

Browse files
authored
Merge branch 'master' into feature/historical-conditions
2 parents 7acce70 + 46ab69f commit 1091ec8

7 files changed

Lines changed: 101 additions & 11 deletions

File tree

e2e/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,13 @@ For those interested in the mechanics of snapshot testing with Playwright, you c
105105

106106
- Our Snapshot tests receive a `@snapshot` tag.
107107
- Snapshots need to be executed within the official Playwright container to ensure we're using the exact rendering platform in CI and locally. To do a valid comparison locally:
108+
- Note, microsoft might have retired older images like `-jammy`. Run the following command to find the available tags:
109+
`curl -s https://mcr.microsoft.com/v2/playwright/tags/list | jq -r '.tags[]' | grep "v{X.X.X}`
108110

109111
```sh
110112
// Replace {X.X.X} with the current Playwright version
111113
// from our package.json configuration file
112-
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
114+
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-jammy /bin/bash
113115
npm install
114116
npm run test:e2e:checksnapshots
115117
```
@@ -123,7 +125,7 @@ To compare a snapshot, run a test and open the html report with the 'Expected' v
123125
```sh
124126
// Replace {X.X.X} with the current Playwright version
125127
// from our package.json configuration file
126-
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-focal /bin/bash
128+
docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v{X.X.X}-jammy /bin/bash
127129
npm install
128130
npm run test:e2e:updatesnapshots
129131
```

e2e/appActions.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,36 @@ async function createStableStateTelemetry(page, parent = 'mine') {
285285
};
286286
}
287287

288+
/**
289+
* Create a Out of order State Telemetry Object (State Generator) for use in visual tests
290+
* and tests against plotting telemetry (e.g. logPlot tests). This will change state every 2 seconds.
291+
* @param {import('@playwright/test').Page} page
292+
* @param {string | import('../src/api/objects/ObjectAPI').Identifier} [parent] the uuid or identifier of the parent object. Defaults to 'mine'
293+
* @returns {Promise<CreatedObjectInfo>} An object containing information about the telemetry object.
294+
*/
295+
async function createOutOfOrderStateTelemetry(page, parent = 'mine', duration = 0.25) {
296+
const parentUrl = await getHashUrlToDomainObject(page, parent);
297+
298+
await page.goto(`${parentUrl}`);
299+
const createdObject = await createDomainObjectWithDefaults(
300+
page,
301+
{
302+
type: 'State Generator',
303+
name: 'Stable State Generator'
304+
},
305+
{ outOfOrder: true, duration: duration.toString() }
306+
);
307+
// Wait until the URL is updated
308+
const uuid = await getFocusedObjectUuid(page);
309+
const url = await getHashUrlToDomainObject(page, uuid);
310+
311+
return {
312+
name: createdObject.name,
313+
uuid,
314+
url
315+
};
316+
}
317+
288318
/**
289319
* Navigates directly to a given object url, in fixed time mode, with the given start and end bounds. Note: does not set
290320
* default view type.
@@ -782,6 +812,7 @@ export {
782812
createDomainObjectWithDefaults,
783813
createExampleTelemetryObject,
784814
createNotification,
815+
createOutOfOrderStateTelemetry,
785816
createPlanFromJSON,
786817
createStableStateTelemetry,
787818
expandEntireTree,

e2e/tests/functional/plugins/plot/plotRendering.e2e.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,11 @@
2727

2828
import {
2929
createDomainObjectWithDefaults,
30+
createOutOfOrderStateTelemetry,
3031
getCanvasPixels,
3132
setRealTimeMode
3233
} from '../../../../appActions.js';
34+
import { VISUAL_REALTIME_URL } from '../../../../constants.js';
3335
import { expect, test } from '../../../../pluginFixtures.js';
3436

3537
test.describe('Plot Rendering', () => {
@@ -95,6 +97,28 @@ test.describe('Plot Rendering', () => {
9597
});
9698
});
9799

100+
test.describe.skip('Plot rendering with out of order data', () => {
101+
let telemetry;
102+
103+
test.beforeEach(async ({ page }) => {
104+
await page.goto(VISUAL_REALTIME_URL, { waitUntil: 'domcontentloaded' });
105+
106+
telemetry = await createOutOfOrderStateTelemetry(page);
107+
});
108+
109+
test('Out of Order Plot Paused', async ({ page, theme }) => {
110+
await page.goto(telemetry.url, { waitUntil: 'domcontentloaded' });
111+
112+
// hover over plot for plot controls
113+
await page.getByLabel('Plot Canvas').hover();
114+
// click on pause control
115+
await page.getByTitle('Pause incoming real-time data').click();
116+
117+
// there should be no out of order data in the plot. This is verified by checking that the out of order y-axis label is not present in the plot. If the out of order data is present, the y-axis label will be present in the plot.
118+
await expect(page.getByText('OUT OF ORDER', { exact: true })).toHaveCount(0);
119+
});
120+
});
121+
98122
/**
99123
* This function edits a sine wave generator with the default options and enables the infinity values option.
100124
*

example/generator/GeneratorMetadataProvider.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ const METADATA_BY_TYPE = {
106106
{
107107
value: 1,
108108
string: 'ON'
109+
},
110+
{
111+
value: 99,
112+
string: 'OUT OF ORDER'
109113
}
110114
],
111115
filters: [

example/generator/StateGeneratorProvider.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@ export default class StateGeneratorProvider {
3535

3636
subscribe(domainObject, callback, options) {
3737
const duration = domainObject.telemetry.duration * 1000;
38-
38+
let tick = 0;
3939
const interval = setInterval(() => {
40-
const now = this.openmct.time.now() || Date.now();
41-
const datum = this.#pointForTimestamp(now, duration, domainObject.name);
40+
tick += 1;
41+
let now = this.openmct.time.now() || Date.now();
42+
let flip = false;
43+
if (domainObject.telemetry.outOfOrder && tick % 3 === 0) {
44+
// 2 steps forward, 1 step back by duration * 2 to simulate out of order data
45+
now -= duration * 1.5;
46+
flip = true;
47+
}
48+
const datum = this.#pointForTimestamp(now, duration, domainObject.name, flip);
4249

4350
if (!this.#shouldBeFiltered(datum, options)) {
4451
datum.value = String(datum.value);
@@ -73,12 +80,15 @@ export default class StateGeneratorProvider {
7380
return Promise.resolve(data);
7481
}
7582

76-
#pointForTimestamp(timestamp, duration, name) {
83+
#pointForTimestamp(timestamp, duration, name, flip = false) {
7784
const key = this.openmct.time.getTimeSystem()?.key || 'utc';
7885
const point = {
7986
name: name,
8087
value: Math.floor(timestamp / duration) % 2
8188
};
89+
if (flip) {
90+
point.value = 99;
91+
}
8292
point[key] = Math.floor(timestamp / duration) * duration;
8393
return point;
8494
}

example/generator/plugin.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,20 @@ export default function (openmct) {
4141
key: 'duration',
4242
required: true,
4343
property: ['telemetry', 'duration']
44+
},
45+
{
46+
name: 'Out of order data',
47+
control: 'toggleSwitch',
48+
cssClass: 'l-input',
49+
key: 'outOfOrder',
50+
required: true,
51+
property: ['telemetry', 'outOfOrder']
4452
}
4553
],
4654
initialize: function (object) {
4755
object.telemetry = {
48-
duration: 5
56+
duration: 5,
57+
outOfOrder: false
4958
};
5059
}
5160
});

src/plugins/plot/configuration/PlotSeries.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ export default class PlotSeries extends Model {
214214
this.unsubscribe = this.openmct.telemetry.subscribe(
215215
this.domainObject,
216216
(data) => {
217-
this.addAll(data, true);
217+
// We cannot assume that the incoming data is chronologically sound, so sorted = false
218+
this.addAll(_(data).sortBy(this.getXVal).value(), false);
218219
},
219220
{
220221
filters: this.filters,
@@ -373,6 +374,7 @@ export default class PlotSeries extends Model {
373374
* @private
374375
*/
375376
sortedIndex(point) {
377+
// lodash's sortedIndexBy uses binary search, so it should be quite efficient for most cases.
376378
return _.sortedIndexBy(this.getSeriesData(), point, this.getXVal);
377379
}
378380
/**
@@ -436,26 +438,34 @@ export default class PlotSeries extends Model {
436438
let insertIndex = data.length;
437439
const currentYVal = this.getYVal(newData);
438440
const lastYVal = this.getYVal(data[insertIndex - 1]);
441+
const currentXVal = this.getXVal(newData);
439442

440443
if (this.isValueInvalid(currentYVal) && this.isValueInvalid(lastYVal)) {
441444
console.warn(`[Plot] Invalid Y Values detected: ${currentYVal} ${lastYVal}`);
442445

443446
return;
444447
}
445448

446-
if (!sorted) {
449+
// if the first new data point has an X value > the last data point we already have, stick it at the end.
450+
const isDataInThePast = currentXVal <= this.getXVal(data[insertIndex - 1]);
451+
if (!sorted && isDataInThePast) {
447452
insertIndex = this.sortedIndex(newData);
448-
if (this.getXVal(data[insertIndex]) === this.getXVal(newData)) {
453+
if (this.getXVal(data[insertIndex]) === currentXVal) {
449454
return;
450455
}
451456

452-
if (this.getXVal(data[insertIndex - 1]) === this.getXVal(newData)) {
457+
if (this.getXVal(data[insertIndex - 1]) === currentXVal) {
453458
return;
454459
}
455460
}
456461

457462
this.updateStats(newData);
458463
newData.mctLimitState = this.evaluate(newData);
464+
// Note: Splicing is a performance bottleneck for large data sets when the insert index
465+
// is NOT at the end of the array, this is because inserting into the middle of an array requires
466+
// shifting all subsequent elements. For now, leave it as is since for the
467+
// majority of cases real-time data will be appended to the end of the array,
468+
// and historical data will be added in sorted order.
459469
data.splice(insertIndex, 0, newData);
460470
this.updateSeriesData(data);
461471
this.emit('add', newData, insertIndex, this);

0 commit comments

Comments
 (0)