| /* eslint-disable dot-notation */ |
| import { assert } from 'chai'; |
| import fetchMock from 'fetch-mock'; |
| import { |
| ColumnHeader, |
| CommitNumber, |
| FrameResponse, |
| QueryConfig, |
| TimestampSeconds, |
| Trace, |
| TraceSet, |
| } from '../json'; |
| import { deepCopy } from '../../../infra-sk/modules/object'; |
| import { |
| calculateRangeChange, |
| defaultPointSelected, |
| ExploreSimpleSk, |
| isValidSelection, |
| PointSelected, |
| selectionToEvent, |
| CommitRange, |
| GraphConfig, |
| updateShortcut, |
| State, |
| } from './explore-simple-sk'; |
| import { setUpElementUnderTest } from '../../../infra-sk/modules/test_util'; |
| import { generateFullDataFrame } from '../dataframe/test_utils'; |
| import sinon from 'sinon'; |
| |
| fetchMock.config.overwriteRoutes = true; |
| |
| const now = 1726081856; // an arbitrary UNIX time; |
| const timeSpan = 89; // an arbitrary prime number for time span between commits . |
| |
| describe('calculateRangeChange', () => { |
| const offsets: CommitRange = [100, 120] as CommitRange; |
| |
| it('finds a left range increase', () => { |
| const zoom: CommitRange = [-1, 10] as CommitRange; |
| const clampedZoom: CommitRange = [0, 10] as CommitRange; |
| |
| const ret = calculateRangeChange(zoom, clampedZoom, offsets); |
| assert.isTrue(ret.rangeChange); |
| |
| // We shift the beginning of the range by RANGE_CHANGE_ON_ZOOM_PERCENT of |
| // the total range. |
| assert.deepEqual(ret.newOffsets, [90, 120]); |
| }); |
| |
| it('finds a right range increase', () => { |
| const zoom: CommitRange = [0, 12] as CommitRange; |
| const clampedZoom: CommitRange = [0, 10] as CommitRange; |
| |
| const ret = calculateRangeChange(zoom, clampedZoom, offsets); |
| assert.isTrue(ret.rangeChange); |
| |
| // We shift the end of the range by RANGE_CHANGE_ON_ZOOM_PERCENT of the |
| // total range. |
| assert.deepEqual(ret.newOffsets, [100, 130]); |
| }); |
| |
| it('find an increase in the range in both directions', () => { |
| const zoom: CommitRange = [-1, 11] as CommitRange; |
| const clampedZoom: CommitRange = [0, 10] as CommitRange; |
| |
| const ret = calculateRangeChange(zoom, clampedZoom, offsets); |
| assert.isTrue(ret.rangeChange); |
| |
| // We shift both the begin and end of the range by |
| // RANGE_CHANGE_ON_ZOOM_PERCENT of the total range. |
| assert.deepEqual(ret.newOffsets, [90, 130]); |
| }); |
| |
| it('find an increase in the range in both directions and clamps the offset', () => { |
| const zoom: CommitRange = [-1, 11] as CommitRange; |
| const clampedZoom: CommitRange = [0, 10] as CommitRange; |
| const widerOffsets: CommitRange = [0, 100] as CommitRange; |
| |
| const ret = calculateRangeChange(zoom, clampedZoom, widerOffsets); |
| assert.isTrue(ret.rangeChange); |
| |
| // We shift both the begin and end of the range by |
| // RANGE_CHANGE_ON_ZOOM_PERCENT of the total range. |
| assert.deepEqual(ret.newOffsets, [0, 150]); |
| }); |
| |
| it('does not find a range change', () => { |
| const zoom: CommitRange = [0, 10] as CommitRange; |
| const clampedZoom: CommitRange = [0, 10] as CommitRange; |
| |
| const ret = calculateRangeChange(zoom, clampedZoom, offsets); |
| assert.isFalse(ret.rangeChange); |
| }); |
| }); |
| |
| // this function is needed to support other unit tests |
| describe('applyFuncToTraces', () => { |
| window.perf = { |
| instance_url: '', |
| radius: 2, |
| key_order: null, |
| num_shift: 50, |
| interesting: 2, |
| step_up_only: false, |
| commit_range_url: '', |
| demo: true, |
| display_group_by: false, |
| hide_list_of_commits_on_explore: false, |
| notifications: 'none', |
| fetch_chrome_perf_anomalies: false, |
| feedback_url: '', |
| chat_url: '', |
| help_url_override: '', |
| trace_format: '', |
| need_alert_action: false, |
| bug_host_url: '', |
| git_repo_url: '', |
| keys_for_commit_range: [], |
| keys_for_useful_links: [], |
| skip_commit_detail_display: false, |
| image_tag: 'fake-tag', |
| remove_default_stat_value: false, |
| enable_skia_bridge_aggregation: false, |
| show_json_file_display: false, |
| always_show_commit_info: false, |
| show_triage_link: true, |
| show_bisect_btn: true, |
| }; |
| |
| // Create a common element-sk to be used by all the tests. |
| const explore = document.createElement('explore-simple-sk') as ExploreSimpleSk; |
| document.body.appendChild(explore); |
| }); |
| |
| describe('PointSelected', () => { |
| it('defaults to not having a name', () => { |
| const p = defaultPointSelected(); |
| assert.isEmpty(p.name); |
| }); |
| |
| it('defaults to being invalid', () => { |
| const p = defaultPointSelected(); |
| assert.isFalse(isValidSelection(p)); |
| }); |
| |
| it('becomes a valid event if the commit appears in the header', () => { |
| const header: ColumnHeader[] = [ |
| { |
| offset: CommitNumber(99), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| { |
| offset: CommitNumber(100), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| { |
| offset: CommitNumber(101), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| ]; |
| |
| const p: PointSelected = { |
| commit: CommitNumber(100), |
| name: 'foo', |
| }; |
| // selectionToEvent will look up the commit (aka offset) in header and |
| // should return an event where the 'x' value is the index of the matching |
| // ColumnHeader in 'header', i.e. 1. |
| const e = selectionToEvent(p, header); |
| assert.equal(e.detail.x, 1); |
| }); |
| |
| it('becomes an invalid event if the commit does not appear in the header', () => { |
| const header: ColumnHeader[] = [ |
| { |
| offset: CommitNumber(99), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| { |
| offset: CommitNumber(100), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| { |
| offset: CommitNumber(101), |
| timestamp: TimestampSeconds(0), |
| author: '', |
| hash: '', |
| message: '', |
| url: '', |
| }, |
| ]; |
| |
| const p: PointSelected = { |
| commit: CommitNumber(102), |
| name: 'foo', |
| }; |
| // selectionToEvent will look up the commit (aka offset) in header and |
| // should return an event where the 'x' value is -1 since the matching |
| // ColumnHeader in 'header' doesn't exist. |
| const e = selectionToEvent(p, header); |
| assert.equal(e.detail.x, -1); |
| }); |
| }); |
| |
| describe('updateShortcut', () => { |
| it('should return empty shortcut if graph configs are absent', async () => { |
| const shortcut = await updateShortcut([]); |
| assert.equal(shortcut, ''); |
| }); |
| |
| it('should return shortcut for non empty graph list', async () => { |
| const defaultConfig: QueryConfig = { |
| default_param_selections: null, |
| default_url_values: null, |
| include_params: null, |
| }; |
| |
| const defaultBody = JSON.stringify(defaultConfig); |
| fetchMock.get('path:/_/defaults/', { |
| status: 200, |
| body: defaultBody, |
| }); |
| |
| fetchMock.post('/_/count/', { |
| count: 117, |
| paramset: {}, |
| }); |
| |
| fetchMock.get(/_\/initpage\/.*/, () => ({ |
| dataframe: { |
| traceset: null, |
| header: null, |
| paramset: {}, |
| skip: 0, |
| }, |
| ticks: [], |
| skps: [], |
| msg: '', |
| })); |
| |
| fetchMock.postOnce('/_/shortcut/update', { id: '12345' }); |
| fetchMock.flush(true); |
| |
| const shortcut = await updateShortcut([ |
| { keys: '', queries: [], formulas: [] }, |
| ] as GraphConfig[]); |
| |
| assert.deepEqual(shortcut, '12345'); |
| }); |
| }); |
| |
| describe('createGraphConfigs', () => { |
| it('traceset without formulas', () => { |
| const traceset = TraceSet({ |
| ',config=8888,arch=x86,': Trace([0.1, 0.2, 0.0, 0.4]), |
| ',config=8888,arch=arm,': Trace([1.1, 1.2, 0.0, 1.4]), |
| ',config=565,arch=x86,': Trace([0.0, 0.0, 3.3, 3.4]), |
| ',config=565,arch=arm,': Trace([0.0, 0.0, 4.3, 4.4]), |
| }); |
| const explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| const result = explore.createGraphConfigs(traceset); |
| const expected: GraphConfig[] = [ |
| { |
| keys: '', |
| formulas: [], |
| queries: ['config=8888&arch=x86'], |
| }, |
| { |
| keys: '', |
| formulas: [], |
| queries: ['config=8888&arch=arm'], |
| }, |
| { |
| keys: '', |
| formulas: [], |
| queries: ['config=565&arch=x86'], |
| }, |
| { |
| keys: '', |
| formulas: [], |
| queries: ['config=565&arch=arm'], |
| }, |
| ]; |
| |
| assert.deepEqual(result, expected); |
| }); |
| |
| it('traceset with formulas', () => { |
| const traceset = TraceSet({ |
| 'func1(,config=8888,arch=x86,)': Trace([0.1, 0.2, 0.0, 0.4]), |
| 'func2(,config=8888,arch=arm,)': Trace([1.1, 1.2, 0.0, 1.4]), |
| }); |
| const explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| const result = explore.createGraphConfigs(traceset); |
| const expected: GraphConfig[] = [ |
| { |
| keys: '', |
| formulas: ['func1(,config=8888,arch=x86,)'], |
| queries: [], |
| }, |
| { |
| keys: '', |
| formulas: ['func2(,config=8888,arch=arm,)'], |
| queries: [], |
| }, |
| ]; |
| |
| assert.deepEqual(result, expected); |
| }); |
| }); |
| |
| describe('Default values', () => { |
| beforeEach(() => { |
| fetchMock.get('/_/login/status', { |
| email: 'someone@example.org', |
| roles: ['editor'], |
| }); |
| fetchMock.post('/_/count/', { |
| count: 117, |
| paramset: {}, |
| }); |
| fetchMock.get(/_\/initpage\/.*/, () => ({ |
| dataframe: { |
| traceset: null, |
| header: null, |
| paramset: {}, |
| skip: 0, |
| }, |
| ticks: [], |
| skps: [], |
| msg: '', |
| })); |
| }); |
| it('Checks no default values', async () => { |
| const defaultConfig: QueryConfig = { |
| default_param_selections: null, |
| default_url_values: null, |
| include_params: null, |
| }; |
| |
| const defaultBody = JSON.stringify(defaultConfig); |
| fetchMock.get('path:/_/defaults/', { |
| status: 200, |
| body: defaultBody, |
| }); |
| |
| const explore = await setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| await fetchMock.flush(true); |
| const originalState = deepCopy(explore!.state); |
| await explore['applyQueryDefaultsIfMissing'](); |
| |
| const newState = explore.state; |
| assert.deepEqual(newState, originalState); |
| }); |
| |
| it('Checks for default summary value', async () => { |
| const defaultConfig: QueryConfig = { |
| default_param_selections: null, |
| default_url_values: { |
| summary: 'true', |
| }, |
| include_params: null, |
| }; |
| |
| const explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| explore['_defaults'] = defaultConfig; |
| |
| const originalState = deepCopy(explore.state); |
| await explore['applyQueryDefaultsIfMissing'](); |
| |
| const newState = explore.state; |
| assert.notDeepEqual(newState, originalState, 'new state should not equal original state'); |
| assert.isTrue(newState.summary); |
| }); |
| }); |
| |
| describe('plotSummary', () => { |
| it('Populate Plot Summary bar', async () => { |
| const explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| |
| explore.state.plotSummary = true; |
| explore['tracesRendered'] = true; |
| explore.render(); |
| |
| const plotSummaryElement = explore['plotSummary'].value; |
| assert.notEqual(plotSummaryElement, undefined); |
| }); |
| |
| it('Plot Summary bar not enabled', async () => { |
| const explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| explore.render(); |
| |
| const plotSummaryElement = explore['plotSummary'].value; |
| assert.equal(plotSummaryElement, undefined); |
| }); |
| }); |
| |
| describe('updateBrowserURL', () => { |
| let explore: ExploreSimpleSk; |
| |
| beforeEach(() => { |
| explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| }); |
| |
| afterEach(() => { |
| fetchMock.reset(); // Reset fetch mocks |
| }); |
| |
| it('should add begin, end, and request_type=0 params when none exist', () => { |
| explore.state.begin = 100; |
| explore.state.end = 200; |
| explore['updateBrowserURL'](); |
| const pushedUrl = new URL(window.location.href as string); |
| assert.equal(pushedUrl.searchParams.get('begin'), '100'); |
| assert.equal(pushedUrl.searchParams.get('end'), '200'); |
| assert.equal(pushedUrl.searchParams.get('request_type'), '0'); |
| }); |
| }); |
| |
| describe('rationalizeTimeRange', () => { |
| let explore: ExploreSimpleSk; |
| let clock: sinon.SinonFakeTimers; |
| const now = 1672531200; // Jan 1, 2023 in seconds |
| |
| beforeEach(() => { |
| explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| clock = sinon.useFakeTimers(now * 1000); |
| }); |
| |
| afterEach(() => { |
| clock.restore(); |
| }); |
| |
| it('handles uninitialized begin and end', () => { |
| const state = new State(); |
| const rationalizedState = explore['rationalizeTimeRange'](state); |
| assert.equal(rationalizedState.end, now); |
| assert.closeTo(rationalizedState.begin, now - 24 * 60 * 60, 1); |
| }); |
| |
| it('corrects inverted time ranges', () => { |
| const state = new State(); |
| state.begin = now - 100; |
| state.end = now - 500; |
| const rationalizedState = explore['rationalizeTimeRange'](state); |
| assert.isTrue(rationalizedState.end > rationalizedState.begin); |
| }); |
| |
| it('handles zero-length time ranges', () => { |
| const state = new State(); |
| state.begin = now - 100; |
| state.end = now - 100; |
| const rationalizedState = explore['rationalizeTimeRange'](state); |
| assert.isTrue(rationalizedState.end > rationalizedState.begin); |
| }); |
| |
| it('ensures end is not in the future', () => { |
| const state = new State(); |
| state.begin = now - 100; |
| state.end = now + 500; |
| const rationalizedState = explore['rationalizeTimeRange'](state); |
| assert.equal(rationalizedState.end, now); |
| }); |
| }); |
| |
| describe('updateTestPickerUrl', () => { |
| let explore: ExploreSimpleSk; |
| |
| beforeEach(async () => { |
| // Mock all fetches that can be triggered by element creation. |
| fetchMock.get(/.*\/_\/initpage\/.*/, { |
| dataframe: { paramset: {} }, |
| }); |
| fetchMock.get('/_/login/status', { |
| email: 'someone@example.org', |
| roles: ['editor'], |
| }); |
| |
| explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| await fetchMock.flush(true); |
| }); |
| |
| afterEach(() => { |
| fetchMock.reset(); |
| }); |
| |
| it('should set the URL to "#" when there are no queries, formulas, or keys', (done) => { |
| explore.state.queries = []; |
| explore.state.formulas = []; |
| explore.state.keys = ''; |
| explore['updateTestPickerUrl'](); |
| setTimeout(() => { |
| assert.equal(explore['testPickerUrl'], '#'); |
| done(); |
| }); |
| }); |
| |
| it('should construct the correct URL when there are queries', (done) => { |
| fetchMock.post('/_/shortcut/update', { id: 'shortcut123' }); |
| explore.state.queries = ['config=test']; |
| explore.state.formulas = []; |
| explore.state.keys = ''; |
| explore.state.begin = 123; |
| explore.state.end = 456; |
| explore.state.requestType = 0; |
| |
| explore['updateTestPickerUrl'](); |
| setTimeout(() => { |
| assert.equal( |
| explore['testPickerUrl'], |
| '/m/?begin=123&end=456&request_type=0&shortcut=shortcut123&totalGraphs=1' |
| ); |
| done(); |
| }); |
| }); |
| }); |
| |
| describe('Incremental Trace Loading', () => { |
| let explore: ExploreSimpleSk; |
| |
| // Define clean queries and comma-wrapped keys separately. |
| const initialQuery = 'arch=x86&config=original'; |
| const initialTraceKey = `,${initialQuery},`; // Comma-wrapped for traceset |
| const newQuery = 'arch=arm&config=new'; |
| const newTraceKey = `,${newQuery},`; // Comma-wrapped for traceset |
| |
| // Generate a dataframe and ensure it has traceMetadata. |
| const initialDataFrame = generateFullDataFrame( |
| { begin: 1, end: 100 }, |
| now, |
| 1, |
| [timeSpan], |
| [[10, 20]], |
| [initialTraceKey] |
| ); |
| initialDataFrame.traceMetadata = []; // Ensure traceMetadata exists. |
| |
| const initialFrameResponse: FrameResponse = { |
| dataframe: initialDataFrame, |
| anomalymap: {}, |
| skps: [], |
| msg: '', |
| display_mode: 'display_plot', |
| }; |
| |
| // Generate the second dataframe and ensure it has traceMetadata. |
| const newDataFrame = generateFullDataFrame( |
| { begin: 1, end: 100 }, |
| now, |
| 1, |
| [timeSpan], |
| [[30, 40]], |
| [newTraceKey] |
| ); |
| newDataFrame.traceMetadata = []; // Ensure traceMetadata exists. |
| |
| const newFrameResponse: FrameResponse = { |
| dataframe: newDataFrame, |
| anomalymap: {}, |
| skps: [], |
| msg: '', |
| display_mode: 'display_plot', |
| }; |
| |
| beforeEach(() => { |
| fetchMock.reset(); |
| explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| |
| fetchMock.get('/_/login/status', { |
| email: 'someone@example.org', |
| roles: ['editor'], |
| }); |
| fetchMock.get('path:/_/defaults/', { |
| status: 200, |
| body: JSON.stringify({}), |
| }); |
| fetchMock.post('/_/frame/start', {}); |
| fetchMock.post('/_/user_issues/', { UserIssues: [] }); |
| fetchMock.flush(true); |
| }); |
| |
| it('only fetches new data when adding a trace', async () => { |
| // Original data. |
| fetchMock.postOnce('/_/frame/start', (_url, opts) => { |
| const body = JSON.parse(opts.body as string); |
| assert.deepEqual(body.queries, [initialQuery]); |
| return { |
| status: 'Finished', |
| results: initialFrameResponse, |
| messages: [], |
| }; |
| }); |
| await explore.addFromQueryOrFormula(true, 'query', initialQuery, ''); |
| await fetchMock.flush(true); |
| |
| // Verify the initial data is present. |
| assert.containsAllKeys(explore['_dataframe'].traceset, [initialTraceKey]); |
| assert.lengthOf(Object.keys(explore['_dataframe'].traceset), 1); |
| |
| // New data, expect incremental fetch. |
| fetchMock.postOnce('/_/frame/start', (_url, opts) => { |
| const body = JSON.parse(opts.body as string); |
| assert.deepEqual(body.queries, [newQuery]); |
| return { |
| status: 'Finished', |
| results: newFrameResponse, |
| messages: [], |
| }; |
| }); |
| |
| await explore.addFromQueryOrFormula(false, 'query', newQuery, ''); |
| await fetchMock.flush(true); |
| |
| const finalTraceset = explore['_dataframe'].traceset; |
| assert.containsAllKeys(finalTraceset, [initialTraceKey, newTraceKey]); |
| assert.lengthOf(Object.keys(finalTraceset), 2); |
| }); |
| }); |
| |
| describe('State Management', () => { |
| let explore: ExploreSimpleSk; |
| let updateTestPickerUrlStub: sinon.SinonStub; |
| |
| beforeEach(async () => { |
| // Mock all fetches that can be triggered by element creation. |
| fetchMock.get(/.*\/_\/initpage\/.*/, { |
| dataframe: { paramset: {} }, |
| }); |
| fetchMock.get('/_/login/status', { |
| email: 'someone@example.org', |
| roles: ['editor'], |
| }); |
| fetchMock.post('/_/frame/start', { |
| status: 'Finished', |
| results: { dataframe: { traceset: {} } }, |
| }); |
| |
| explore = setUpElementUnderTest<ExploreSimpleSk>('explore-simple-sk')(); |
| await fetchMock.flush(true); |
| |
| // Stub the method that gets called when a state change is detected. |
| updateTestPickerUrlStub = sinon.stub(explore, 'updateTestPickerUrl' as any); |
| }); |
| |
| afterEach(() => { |
| fetchMock.reset(); |
| sinon.restore(); |
| }); |
| |
| it('detects query changes with special characters due to encoding', () => { |
| const state1 = new State(); |
| state1.queries = ['config=a b']; |
| explore.state = state1; |
| |
| // Reset the stub after the initial state setting. |
| updateTestPickerUrlStub.resetHistory(); |
| |
| const state2 = new State(); |
| state2.queries = ['config=a+b']; |
| explore.state = state2; |
| |
| // 'a b' and 'a+b' are different strings, so a change should be detected. |
| assert.isTrue( |
| updateTestPickerUrlStub.called, |
| "URL update should be called for different queries ('a b' vs 'a+b')" |
| ); |
| |
| updateTestPickerUrlStub.resetHistory(); |
| |
| const state3 = new State(); |
| state3.queries = ['config=a+b']; |
| explore.state = state3; |
| |
| // The queries are identical, so no change should be detected. |
| assert.isFalse( |
| updateTestPickerUrlStub.called, |
| 'URL update should not be called for identical queries' |
| ); |
| }); |
| }); |