Add UI in analysis task page to show commit testability information.
authordewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 17 Jan 2019 05:56:18 +0000 (05:56 +0000)
committerdewei_zhu@apple.com <dewei_zhu@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 17 Jan 2019 05:56:18 +0000 (05:56 +0000)
https://bugs.webkit.org/show_bug.cgi?id=192972

Reviewed by Ryosuke Niwa.

Add UI in custom analysis task configuration and customizable test group form to show testability information.
Fix a bug in 'CustomAnalysisTaskConfigurator._updateCommitSetMap' that 'currentComparison' is incorrectly set.
SQL to update existing database:
    ALTER TABLE commits ADD COLUMN IF NOT EXISTS commit_testability varchar(128) DEFAULT NULL;

* browser-tests/custom-analysis-task-configurator-tests.js: Added a unit test for the bug in
'CustomAnalysisTaskConfigurator._updateCommitSetMap'.
Added a unit test to make sure 'CustomAnalysisTaskConfigurator' still works when commit fetching never returns.
* browser-tests/index.html: Imported ''custom-analysis-task-configurator-tests.js'.
* init-database.sql: Increase 'commit_testability' field length from 64 characters to 128.
* public/v3/components/custom-analysis-task-configurator.js: Added UI to show testability information.
(CustomAnalysisTaskConfigurator):
(CustomAnalysisTaskConfigurator.prototype._didUpdateSelectedPlatforms): Should reset related field for corresponding
repositories that user does not specify revision.
(CustomAnalysisTaskConfigurator.prototype._updateMapFromSpecifiedRevisionsForConfiguration): A helper function
to update '_specifiedCommits' and '_invalidRevisionsByConfiguration' per '_specifiedRevisions'.
(CustomAnalysisTaskConfigurator.prototype.render):
(CustomAnalysisTaskConfigurator.prototype._updateCommitSetMap): Fixed a bug that 'currentComparison' is incorrectly set.
(CustomAnalysisTaskConfigurator.prototype._computeCommitSet):
(CustomAnalysisTaskConfigurator.prototype.async._fetchCommitsForConfiguration):
(CustomAnalysisTaskConfigurator.prototype.async._resolveRevision):
(CustomAnalysisTaskConfigurator.prototype._buildRevisionTable):
(CustomAnalysisTaskConfigurator.prototype._buildTestabilityList):
(CustomAnalysisTaskConfigurator.prototype._selectRepositoryGroup):
(CustomAnalysisTaskConfigurator.prototype._buildRevisionInput):
(CustomAnalysisTaskConfigurator.cssTemplate):
* public/v3/components/customizable-test-group-form.js: Added UI to show testability information.
(CustomizableTestGroupForm.prototype._renderCustomRevisionTable):
(CustomizableTestGroupForm.prototype._constructTestabilityRows.):
(CustomizableTestGroupForm.prototype._constructTestabilityRows):
(CustomizableTestGroupForm.prototype._constructRevisionRadioButtons):
Changing either revision editor or radio button should trigger a re-render as testability
information for updated revision may change.
(CustomizableTestGroupForm.cssTemplate):
* public/v3/models/commit-set.js:
(IntermediateCommitSet.prototype.commitsWithTestability): Renamed from 'commitsWithTestabilityWarnings'.
(IntermediateCommitSet.prototype.commitsWithTestabilityWarnings): Deleted.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@240104 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/browser-tests/custom-analysis-task-configurator-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/browser-tests/index.html
Websites/perf.webkit.org/init-database.sql
Websites/perf.webkit.org/public/v3/components/custom-analysis-task-configurator.js
Websites/perf.webkit.org/public/v3/components/customizable-test-group-form.js
Websites/perf.webkit.org/public/v3/models/commit-set.js

index 0936909..4156c10 100644 (file)
@@ -1,3 +1,48 @@
+2018-12-21  Dewei Zhu  <dewei_zhu@apple.com>
+
+        Add UI in analysis task page to show commit testability information.
+        https://bugs.webkit.org/show_bug.cgi?id=192972
+
+        Reviewed by Ryosuke Niwa.
+
+        Add UI in custom analysis task configuration and customizable test group form to show testability information.
+        Fix a bug in 'CustomAnalysisTaskConfigurator._updateCommitSetMap' that 'currentComparison' is incorrectly set.
+        SQL to update existing database:
+            ALTER TABLE commits ADD COLUMN IF NOT EXISTS commit_testability varchar(128) DEFAULT NULL;
+
+        * browser-tests/custom-analysis-task-configurator-tests.js: Added a unit test for the bug in
+        'CustomAnalysisTaskConfigurator._updateCommitSetMap'.
+        Added a unit test to make sure 'CustomAnalysisTaskConfigurator' still works when commit fetching never returns.
+        * browser-tests/index.html: Imported ''custom-analysis-task-configurator-tests.js'.
+        * init-database.sql: Increase 'commit_testability' field length from 64 characters to 128.
+        * public/v3/components/custom-analysis-task-configurator.js: Added UI to show testability information.
+        (CustomAnalysisTaskConfigurator):
+        (CustomAnalysisTaskConfigurator.prototype._didUpdateSelectedPlatforms): Should reset related field for corresponding
+        repositories that user does not specify revision.
+        (CustomAnalysisTaskConfigurator.prototype._updateMapFromSpecifiedRevisionsForConfiguration): A helper function
+        to update '_specifiedCommits' and '_invalidRevisionsByConfiguration' per '_specifiedRevisions'.
+        (CustomAnalysisTaskConfigurator.prototype.render):
+        (CustomAnalysisTaskConfigurator.prototype._updateCommitSetMap): Fixed a bug that 'currentComparison' is incorrectly set.
+        (CustomAnalysisTaskConfigurator.prototype._computeCommitSet):
+        (CustomAnalysisTaskConfigurator.prototype.async._fetchCommitsForConfiguration):
+        (CustomAnalysisTaskConfigurator.prototype.async._resolveRevision):
+        (CustomAnalysisTaskConfigurator.prototype._buildRevisionTable):
+        (CustomAnalysisTaskConfigurator.prototype._buildTestabilityList):
+        (CustomAnalysisTaskConfigurator.prototype._selectRepositoryGroup):
+        (CustomAnalysisTaskConfigurator.prototype._buildRevisionInput):
+        (CustomAnalysisTaskConfigurator.cssTemplate):
+        * public/v3/components/customizable-test-group-form.js: Added UI to show testability information.
+        (CustomizableTestGroupForm.prototype._renderCustomRevisionTable):
+        (CustomizableTestGroupForm.prototype._constructTestabilityRows.):
+        (CustomizableTestGroupForm.prototype._constructTestabilityRows):
+        (CustomizableTestGroupForm.prototype._constructRevisionRadioButtons):
+        Changing either revision editor or radio button should trigger a re-render as testability
+        information for updated revision may change.
+        (CustomizableTestGroupForm.cssTemplate):
+        * public/v3/models/commit-set.js:
+        (IntermediateCommitSet.prototype.commitsWithTestability): Renamed from 'commitsWithTestabilityWarnings'.
+        (IntermediateCommitSet.prototype.commitsWithTestabilityWarnings): Deleted.
+
 2018-12-14  Dewei Zhu  <dewei_zhu@apple.com>
 
         Extend commits table to contain testability information.
diff --git a/Websites/perf.webkit.org/browser-tests/custom-analysis-task-configurator-tests.js b/Websites/perf.webkit.org/browser-tests/custom-analysis-task-configurator-tests.js
new file mode 100644 (file)
index 0000000..bf23e23
--- /dev/null
@@ -0,0 +1,234 @@
+describe('CustomAnalysisTaskConfigurator', () => {
+    const scripts = ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'models/data-model.js', 'models/commit-log.js',
+        'models/commit-set.js', 'models/repository.js', 'models/metric.js', 'models/triggerable.js', 'models/test.js', 'models/platform.js', 'components/test-group-form.js',
+        'components/custom-analysis-task-configurator.js', 'components/instant-file-uploader.js', 'lazily-evaluated-function.js'];
+
+    async function createCustomAnalysisTaskConfiguratorWithContext(context)
+    {
+        await context.importScripts(scripts, 'ComponentBase', 'DataModelObject', 'Repository', 'CommitLog', 'Platform', 'Test', 'Metric', 'Triggerable',
+            'TriggerableRepositoryGroup', 'CustomCommitSet', 'CustomAnalysisTaskConfigurator', 'MockRemoteAPI', 'LazilyEvaluatedFunction');
+        const customAnalysisTaskConfigurator = new context.symbols.CustomAnalysisTaskConfigurator;
+        context.document.body.appendChild(customAnalysisTaskConfigurator.element());
+        return customAnalysisTaskConfigurator;
+    }
+
+    async function sleep(timeout)
+    {
+        await new Promise((resolve) => setTimeout(() => resolve(), timeout));
+    }
+
+    it('Should be able to schedule A/B test even fetching commit information never returns', async () => {
+        const context = new BrowsingContext();
+        const customAnalysisTaskConfigurator = await createCustomAnalysisTaskConfiguratorWithContext(context);
+        context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval = 1;
+
+        const test = new context.symbols.Test(1, {name: 'Speedometer'});
+        const platform = new context.symbols.Platform(1, {
+            name: 'Mojave',
+            metrics: [
+                new context.symbols.Metric(1, {
+                    name: 'Allocation',
+                    aggregator: 'Arithmetic',
+                    test
+                })
+            ],
+            lastModifiedByMetric: Date.now(),
+        });
+        const repository = context.symbols.Repository.ensureSingleton(1, {name: 'WebKit'});
+        const triggerableRepositoryGroup = new context.symbols.TriggerableRepositoryGroup(1, {repositories: [{repository}]});
+        new context.symbols.Triggerable(1, {
+            name: 'test-triggerable',
+            isDisabled: false,
+            repositoryGroups: [triggerableRepositoryGroup],
+            configurations: [{test, platform}],
+        });
+        customAnalysisTaskConfigurator.selectTests([test]);
+        customAnalysisTaskConfigurator.selectPlatform(platform);
+
+        await waitForComponentsToRender(context);
+
+        const requests = context.symbols.MockRemoteAPI.requests;
+        expect(requests.length).to.be(1);
+        expect(requests[0].url).to.be('/api/commits/1/latest?platform=1');
+        requests[0].reject();
+
+        customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').value = '123';
+        customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').dispatchEvent(new Event('input'));
+        await sleep(context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval);
+        expect(requests.length).to.be(2);
+        expect(requests[1].url).to.be('/api/commits/1/123');
+
+        customAnalysisTaskConfigurator._configureComparison();
+        await waitForComponentsToRender(context);
+
+        customAnalysisTaskConfigurator.content('comparison-revision-table').querySelector('input').value = '456';
+        customAnalysisTaskConfigurator.content('comparison-revision-table').querySelector('input').dispatchEvent(new Event('input'));
+        await sleep(context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval);
+        expect(requests.length).to.be(3);
+        expect(requests[2].url).to.be('/api/commits/1/456');
+
+        const commitSets = customAnalysisTaskConfigurator.commitSets();
+        expect(commitSets.length).to.be(2);
+        expect(commitSets[0].repositories().length).to.be(1);
+        expect(commitSets[0].revisionForRepository(repository)).to.be('123');
+        expect(commitSets[1].repositories().length).to.be(1);
+        expect(commitSets[1].revisionForRepository(repository)).to.be('456');
+
+        context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval = 100;
+    });
+
+    it('Should not update commitSetMap if baseline is set and unmodified but comparison is null', async () => {
+        const context = new BrowsingContext();
+        const customAnalysisTaskConfigurator = await createCustomAnalysisTaskConfiguratorWithContext(context);
+
+        await waitForComponentsToRender(context);
+        const repository = context.symbols.Repository.ensureSingleton(1, {name: 'WebKit'});
+        const commitSet = new context.symbols.CustomCommitSet;
+        commitSet.setRevisionForRepository(repository, '210948', null);
+        customAnalysisTaskConfigurator._commitSetMap = {'Baseline': commitSet, 'Comparison': null};
+        customAnalysisTaskConfigurator._repositoryGroupByConfiguration['Baseline'] = new context.symbols.TriggerableRepositoryGroup(1, {repositories: [{repository}]});
+        customAnalysisTaskConfigurator._specifiedRevisions['Baseline'].set(repository, '210948');
+
+        const originalCommitSet = customAnalysisTaskConfigurator._commitSetMap;
+        await customAnalysisTaskConfigurator._updateCommitSetMap();
+
+        expect(customAnalysisTaskConfigurator._commitSetMap).to.be(originalCommitSet);
+    });
+
+    it('Should preserve user specified revision if user has ever modified the revision', async () => {
+        const context = new BrowsingContext();
+        const customAnalysisTaskConfigurator = await createCustomAnalysisTaskConfiguratorWithContext(context);
+        context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval = 1;
+
+        const test = new context.symbols.Test(1, {name: 'Speedometer'});
+        const mojave = new context.symbols.Platform(1, {
+            name: 'Mojave',
+            metrics: [
+                new context.symbols.Metric(1, {
+                    name: 'Allocation',
+                    aggregator: 'Arithmetic',
+                    test
+                })
+            ],
+            lastModifiedByMetric: Date.now(),
+        });
+        const highSierra = new context.symbols.Platform(2, {
+            name: 'High Sierra',
+            metrics: [
+                new context.symbols.Metric(1, {
+                    name: 'Allocation',
+                    aggregator: 'Arithmetic',
+                    test
+                })
+            ],
+            lastModifiedByMetric: Date.now(),
+        });
+        const repository = context.symbols.Repository.ensureSingleton(1, {name: 'WebKit'});
+        const triggerableRepositoryGroup = new context.symbols.TriggerableRepositoryGroup(1, {repositories: [{repository}]});
+        new context.symbols.Triggerable(1, {
+            name: 'test-triggerable',
+            isDisabled: false,
+            repositoryGroups: [triggerableRepositoryGroup],
+            configurations: [{test, platform: mojave}, {test, platform: highSierra}],
+        });
+        customAnalysisTaskConfigurator.selectTests([test]);
+        customAnalysisTaskConfigurator.selectPlatform(mojave);
+
+        await waitForComponentsToRender(context);
+
+        const requests = context.symbols.MockRemoteAPI.requests;
+        expect(requests.length).to.be(1);
+        expect(requests[0].url).to.be('/api/commits/1/latest?platform=1');
+        requests[0].reject();
+
+        customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').value = '123';
+        customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').dispatchEvent(new Event('input'));
+        await sleep(context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval);
+        expect(requests.length).to.be(2);
+        expect(requests[1].url).to.be('/api/commits/1/123');
+
+        customAnalysisTaskConfigurator._configureComparison();
+        await waitForComponentsToRender(context);
+
+        customAnalysisTaskConfigurator.content('comparison-revision-table').querySelector('input').value = '456';
+        customAnalysisTaskConfigurator.content('comparison-revision-table').querySelector('input').dispatchEvent(new Event('input'));
+        await sleep(context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval);
+        expect(requests.length).to.be(3);
+        expect(requests[2].url).to.be('/api/commits/1/456');
+
+        let commitSets = customAnalysisTaskConfigurator.commitSets();
+        expect(commitSets.length).to.be(2);
+        expect(commitSets[0].repositories().length).to.be(1);
+        expect(commitSets[0].revisionForRepository(repository)).to.be('123');
+        expect(commitSets[1].repositories().length).to.be(1);
+        expect(commitSets[1].revisionForRepository(repository)).to.be('456');
+
+        customAnalysisTaskConfigurator.selectPlatform(highSierra);
+        await waitForComponentsToRender(context);
+
+        commitSets = customAnalysisTaskConfigurator.commitSets();
+        expect(commitSets.length).to.be(2);
+        expect(commitSets[0].repositories().length).to.be(1);
+        expect(commitSets[0].revisionForRepository(repository)).to.be('123');
+        expect(commitSets[1].repositories().length).to.be(1);
+        expect(commitSets[1].revisionForRepository(repository)).to.be('456');
+        context.symbols.CustomAnalysisTaskConfigurator.commitFetchInterval = 100;
+    });
+
+    it('Should reset commit set if user has never modified the revision', async () => {
+        const context = new BrowsingContext();
+        const customAnalysisTaskConfigurator = await createCustomAnalysisTaskConfiguratorWithContext(context);
+
+        const test = new context.symbols.Test(1, {name: 'Speedometer'});
+        const mojave = new context.symbols.Platform(1, {
+            name: 'Mojave',
+            metrics: [
+                new context.symbols.Metric(1, {
+                    name: 'Allocation',
+                    aggregator: 'Arithmetic',
+                    test
+                })
+            ],
+            lastModifiedByMetric: Date.now(),
+        });
+        const highSierra = new context.symbols.Platform(2, {
+            name: 'High Sierra',
+            metrics: [
+                new context.symbols.Metric(1, {
+                    name: 'Allocation',
+                    aggregator: 'Arithmetic',
+                    test
+                })
+            ],
+            lastModifiedByMetric: Date.now(),
+        });
+        const repository = context.symbols.Repository.ensureSingleton(1, {name: 'WebKit'});
+        const triggerableRepositoryGroup = new context.symbols.TriggerableRepositoryGroup(1, {name: 'test-triggerable', repositories: [{repository}]});
+        new context.symbols.Triggerable(1, {
+            name: 'test-triggerable',
+            isDisabled: false,
+            repositoryGroups: [triggerableRepositoryGroup],
+            configurations: [{test, platform: mojave}, {test, platform: highSierra}],
+        });
+        customAnalysisTaskConfigurator.selectTests([test]);
+        customAnalysisTaskConfigurator.selectPlatform(mojave);
+        await waitForComponentsToRender(context);
+
+        const requests = context.symbols.MockRemoteAPI.requests;
+        expect(requests.length).to.be(1);
+        expect(requests[0].url).to.be('/api/commits/1/latest?platform=1');
+        requests[0].resolve({commits: [{
+            id: 1,
+            revision: '123',
+            repository: repository.id(),
+            time: Date.now(),
+        }]});
+        await waitForComponentsToRender(context);
+
+        expect(customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').value).to.be('123');
+        customAnalysisTaskConfigurator.selectPlatform(highSierra);
+
+        await waitForComponentsToRender(context);
+        expect(customAnalysisTaskConfigurator.content('baseline-revision-table').querySelector('input').value).to.be('');
+    });
+});
\ No newline at end of file
index 354047d..62fe992 100644 (file)
@@ -27,6 +27,7 @@ mocha.setup('bdd');
 <script src="chart-revision-range-tests.js"></script>
 <script src="commit-log-viewer-tests.js"></script>
 <script src="test-group-form-tests.js"></script>
+<script src="custom-analysis-task-configurator-tests.js"></script>
 <script src="customizable-test-group-form-tests.js"></script>
 <script src="markup-page-tests.js"></script>
 <script src="test-group-result-page-tests.js"></script>
index c7b7d60..cb8845c 100644 (file)
@@ -101,7 +101,7 @@ CREATE TABLE commits (
     commit_committer integer REFERENCES committers ON DELETE CASCADE,
     commit_message text,
     commit_reported boolean NOT NULL DEFAULT FALSE,
-    commit_testability varchar(64) DEFAULT NULL,
+    commit_testability varchar(128) DEFAULT NULL,
     CONSTRAINT commit_in_repository_must_be_unique UNIQUE(commit_repository, commit_revision));
 CREATE INDEX commit_time_index ON commits(commit_time);
 CREATE INDEX commit_order_index ON commits(commit_order);
index 43f35b7..f2d5107 100644 (file)
@@ -7,16 +7,16 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         this._selectedTests = [];
         this._triggerablePlatforms = [];
         this._selectedPlatform = null;
-        this._configurationNames = ['Baseline', 'Comparison'];
         this._showComparison = false;
         this._commitSetMap = {};
         this._specifiedRevisions = {'Baseline': new Map, 'Comparison': new Map};
         this._patchUploaders = {'Baseline': new Map, 'Comparison': new Map};
         this._customRootUploaders = {'Baseline': null, 'Comparison': null};
-        this._fetchedRevisions = {'Baseline': new Map, 'Comparison': new Map};
+        this._fetchedCommits = {'Baseline': new Map, 'Comparison': new Map};
         this._repositoryGroupByConfiguration = {'Baseline': null, 'Comparison': null};
-        this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
+        this._invalidRevisionsByConfiguration = {'Baseline': new Map, 'Comparison': new Map};
 
+        this._updateTriggerableLazily = new LazilyEvaluatedFunction(this._updateTriggerable.bind(this));
         this._renderTriggerableTestsLazily = new LazilyEvaluatedFunction(this._renderTriggerableTests.bind(this));
         this._renderTriggerablePlatformsLazily = new LazilyEvaluatedFunction(this._renderTriggerablePlatforms.bind(this));
         this._renderRepositoryPanesLazily = new LazilyEvaluatedFunction(this._renderRepositoryPanes.bind(this));
@@ -55,11 +55,27 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
 
     _didUpdateSelectedPlatforms()
     {
+        for (const configuration of ['Baseline', 'Comparison']) {
+            this._updateMapFromSpecifiedRevisionsForConfiguration(this._fetchedCommits, configuration);
+            this._updateMapFromSpecifiedRevisionsForConfiguration(this._invalidRevisionsByConfiguration, configuration);
+        }
         this._updateCommitSetMap();
-
         this.enqueueToRender();
     }
 
+    _updateMapFromSpecifiedRevisionsForConfiguration(map, configuration)
+    {
+        const referenceMap = this._specifiedRevisions[configuration];
+        const newValue = new Map;
+        for (const [key, value] of map[configuration].entries()) {
+            if (!referenceMap.has(key))
+                continue;
+            newValue.set(key, value);
+        }
+        if (newValue.size !== map[configuration].size)
+            map[configuration] = newValue;
+    }
+
     setCommitSets(baselineCommitSet, comparisonCommitSet)
     {
         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
@@ -190,6 +206,12 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         const [triggerable, error] = this._updateTriggerableLazily.evaluate(this._selectedTests, this._selectedPlatform);
 
         this._renderRepositoryPanesLazily.evaluate(triggerable, error, this._selectedPlatform, this._repositoryGroupByConfiguration, this._showComparison);
+
+        this.renderReplace(this.content('baseline-testability'), this._buildTestabilityList(this._commitSetMap['Baseline'],
+            'Baseline', this._invalidRevisionsByConfiguration['Baseline']));
+
+        this.renderReplace(this.content('comparison-testability'), !this._showComparison ? null :
+            this._buildTestabilityList(this._commitSetMap['Comparison'], 'Comparison', this._invalidRevisionsByConfiguration['Comparison']));
     }
 
     _renderTriggerableTests()
@@ -304,12 +326,12 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             newComparison = null;
 
         const currentBaseline = this._commitSetMap['Baseline'];
-        const currentComparison = this._commitSetMap['Baseline'];
-        if (newBaseline == currentBaseline && newComparison == currentComparison)
-            return; // Both of them are null.
+        const currentComparison = this._commitSetMap['Comparison'];
+        const areCommitSetsEqual = (commitSetA, commitSetB) => commitSetA == commitSetB || (commitSetA && commitSetB && commitSetA.equals(commitSetB));
+        const sameBaselineConfig = areCommitSetsEqual(currentBaseline, newBaseline);
+        const sameComparisionConfig = areCommitSetsEqual(currentComparison, newComparison);
 
-        if (newBaseline && currentBaseline && newBaseline.equals(currentBaseline)
-            && newComparison && currentComparison && newComparison.equals(currentComparison))
+        if (sameBaselineConfig && sameComparisionConfig)
             return;
 
         this._commitSetMap = {'Baseline': newBaseline, 'Comparison': newComparison};
@@ -331,8 +353,11 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         const commitSet = new CustomCommitSet;
         for (let repository of repositoryGroup.repositories()) {
             let revision = this._specifiedRevisions[configurationName].get(repository);
-            if (!revision)
-                revision = this._fetchedRevisions[configurationName].get(repository);
+            if (!revision) {
+                const commit = this._fetchedCommits[configurationName].get(repository);
+                if (commit)
+                    revision = commit.revision();
+            }
             if (!revision)
                 return null;
             let patch = null;
@@ -355,6 +380,51 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         return commitSet;
     }
 
+    async _fetchCommitsForConfiguration(configurationName)
+    {
+        const commitSet = this._commitSetMap[configurationName];
+        if (!commitSet)
+            return;
+
+        const specifiedRevisions = this._specifiedRevisions[configurationName];
+        const fetchedCommits = this._fetchedCommits[configurationName];
+        const invalidRevisionForRepository = this._invalidRevisionsByConfiguration[configurationName];
+
+        await Promise.all(Array.from(commitSet.repositories()).map((repository) => {
+            const revision = commitSet.revisionForRepository(repository);
+            return this._resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits);
+        }));
+
+        const latestCommitSet = this._commitSetMap[configurationName];
+        if (commitSet != latestCommitSet)
+            return;
+        this.enqueueToRender();
+    }
+
+    async _resolveRevision(repository, revision, specifiedRevisions, invalidRevisionForRepository, fetchedCommits)
+    {
+        const fetchedCommit = fetchedCommits.get(repository);
+        if (fetchedCommit && fetchedCommit.revision() == revision)
+            return;
+
+        fetchedCommits.delete(repository);
+        let commits = [];
+        try {
+            commits = await CommitLog.fetchForSingleRevision(repository, revision);
+        } catch (error) {
+            console.assert(error == 'UnknownCommit');
+            if (revision != specifiedRevisions.get(repository))
+                return;
+            invalidRevisionForRepository.set(repository, revision);
+            return;
+        }
+        console.assert(commits.length, 1);
+        if (revision != specifiedRevisions.get(repository))
+            return;
+        invalidRevisionForRepository.delete(repository);
+        fetchedCommits.set(repository, commits[0]);
+    }
+
     _renderRepositoryPanes(triggerable, error, platform, repositoryGroupByConfiguration, showComparison)
     {
         this.content('repository-configuration-error-pane').style.display = error ? null : 'none';
@@ -414,7 +484,6 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
     _buildRevisionTable(configurationName, repositoryGroups, currentGroup, platform, requiredRepositories, optionalRepositoryList, alwaysAcceptsCustomRoots)
     {
         const element = ComponentBase.createElement;
-        const link = ComponentBase.createLink;
 
         const customRootsTBody = element('tbody', [
             element('tr', [
@@ -453,6 +522,25 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             )];
     }
 
+    _buildTestabilityList(commitSet, configurationName, invalidRevisionForRepository)
+    {
+        const element = ComponentBase.createElement;
+        const entries = [];
+
+        if (!commitSet || !commitSet.repositories().length)
+            return [];
+
+        for (const repository of commitSet.repositories()) {
+            const commit = this._fetchedCommits[configurationName].get(repository);
+            if (commit && commit.testability() && !invalidRevisionForRepository.has(repository))
+                entries.push(element('li', `${commit.repository().name()} - "${commit.label()}": ${commit.testability()}`));
+            if (invalidRevisionForRepository.has(repository))
+                entries.push(element('li', `${repository.name()} - "${invalidRevisionForRepository.get(repository)}": Invalid revision`));
+        }
+
+        return entries;
+    }
+
     _buildRepositoryGroupList(repositoryGroups, currentGroup, configurationName)
     {
         const element = ComponentBase.createElement;
@@ -476,6 +564,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
         clone[configurationName] = group;
         this._repositoryGroupByConfiguration = clone;
         this._updateCommitSetMap();
+        this._fetchCommitsForConfiguration(configurationName);
         this.enqueueToRender();
     }
 
@@ -483,10 +572,19 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
     {
         const revision = this._specifiedRevisions[configurationName].get(repository) || '';
         const element = ComponentBase.createElement;
+        let scheduledUpdate = null;
         const input = element('input', {value: revision, oninput: () => {
             unmodifiedInput = null;
-            this._specifiedRevisions[configurationName].set(repository, input.value);
+            const revisionToFetch = input.value;
+            this._specifiedRevisions[configurationName].set(repository, revisionToFetch);
             this._updateCommitSetMap();
+            if (scheduledUpdate)
+                clearTimeout(scheduledUpdate);
+            scheduledUpdate = setTimeout(() => {
+                if (revisionToFetch == input.value)
+                    this._fetchCommitsForConfiguration(configurationName);
+                scheduledUpdate = null;
+            }, CustomAnalysisTaskConfigurator.commitFetchInterval);
         }});
         let unmodifiedInput = input;
 
@@ -494,7 +592,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             CommitLog.fetchLatestCommitForPlatform(repository, platform).then((commit) => {
                 if (commit && unmodifiedInput) {
                     unmodifiedInput.value = commit.revision();
-                    this._fetchedRevisions[configurationName].set(repository, commit.revision());
+                    this._fetchedCommits[configurationName].set(repository, commit);
                     this._updateCommitSetMap();
                 }
             });
@@ -521,6 +619,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             <section id="baseline-configuration-pane" class="pane">
                 <h2>3. Configure Baseline</h2>
                 <table id="baseline-revision-table" class="revision-table"></table>
+                <ul id="baseline-testability"></ul>
             </section>
             <section id="specify-comparison-pane" class="pane">
                 <button id="specify-comparison-button">Configure to Compare</button>
@@ -528,6 +627,7 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
             <section id="comparison-configuration-pane" class="pane">
                 <h2>4. Configure Comparison</h2>
                 <table id="comparison-revision-table" class="revision-table"></table>
+                <ul id="comparison-testability"></ul>
             </section>`;
     }
 
@@ -674,8 +774,16 @@ class CustomAnalysisTaskConfigurator extends ComponentBase {
                 font-size: 1.2rem;
                 font-weight: inherit;
             }
+
+            #baseline-testability li,
+            #comparison-testability li {
+                color: #c33;
+                width: 20rem;
+            }
 `;
     }
 }
 
+CustomAnalysisTaskConfigurator.commitFetchInterval = 100;
+
 ComponentBase.defineElement('custom-analysis-task-configurator', CustomAnalysisTaskConfigurator);
index a1cd2dc..ea86fc4 100644 (file)
@@ -135,7 +135,33 @@ class CustomizableTestGroupForm extends TestGroupForm {
             element('thead',
                 element('tr',
                     [element('td', {colspan: 2}, 'Repository'), commitSetLabels.map((label) => element('td', {colspan: commitSetLabels.length + 1}, label)), element('td')])),
-            this._constructTableBodyList(repositoryList, commitSetMap, ownedRepositoriesByRepository, this._hasIncompleteOwnedRepository, uncustomizedCommitSetMap)]);
+            this._constructTableBodyList(repositoryList, commitSetMap, ownedRepositoriesByRepository, this._hasIncompleteOwnedRepository, uncustomizedCommitSetMap),
+            this._constructTestabilityRows(commitSetMap)]);
+    }
+
+    _constructTestabilityRows(commitSetMap)
+    {
+        const element = ComponentBase.createElement;
+
+        const commitSets = Array.from(commitSetMap.values());
+        const hasCommitWithTestability = commitSets.some((commitSet) =>  !!commitSet.commitsWithTestability().length);
+        for (const c of commitSets) {
+            if (c.commitsWithTestability().length)
+                console.log(c);
+        }
+        console.log({hasCommitWithTestability});
+        console.log('aaaa');
+        if (!hasCommitWithTestability)
+            return [];
+
+        const testabilityCells = [];
+        for (const commitSet of commitSetMap.values()) {
+            const entries = commitSet.commitsWithTestability().map((commit) =>
+                element('li', `${commit.title()}: ${commit.testability()}`));
+            testabilityCells.push(element('td', {colspan: commitSetMap.size + 1, class: 'testability'}, element('ul', entries)));
+        }
+
+        return element('tbody', element('tr', [element('td', {colspan: 2}), testabilityCells, element('td')]));
     }
 
     _constructTableBodyList(repositoryList, commitSetMap, ownedRepositoriesByRepository, hasIncompleteOwnedRepository, uncustomizedCommitSetMap)
@@ -255,8 +281,8 @@ class CustomizableTestGroupForm extends TestGroupForm {
             onchange: () => {
                 if (ownerRepository)
                     return;
-
-                commitSetMap.get(columnLabel).updateRevisionForOwnerRepository(repository, revisionEditor.value).catch(
+                commitSetMap.get(columnLabel).updateRevisionForOwnerRepository(repository, revisionEditor.value).then(
+                    () => this.enqueueToRender(),
                     () => {
                         alert(`"${revisionEditor.value}" does not exist in "${repository.name()}".`);
                         revisionEditor.value = revision;
@@ -278,6 +304,7 @@ class CustomizableTestGroupForm extends TestGroupForm {
                     revisionEditor.value = uncustomizedCommit ? uncustomizedCommit.revision() : '';
                     if (uncustomizedCommit && uncustomizedCommit.ownerCommit())
                         this._ownerRevisionMap.get(columnLabel).set(repository, uncustomizedCommit.ownerCommit().revision());
+                    this.enqueueToRender();
                 }});
             nodes.push(element('td', element('label', [radioButton, labelToChoose])));
         }
@@ -336,6 +363,18 @@ class CustomizableTestGroupForm extends TestGroupForm {
             #notify-on-completion-checkbox {
                 margin-left: 0.4rem;
             }
+
+            #custom-table td.testability {
+                vertical-align: top;
+            }
+
+            #custom-table td.testability ul {
+                text-align: left;
+                color: #c33;
+                max-width: 13rem;
+                margin: 0 0 0 1rem;
+                padding: 0;
+            }
             `;
     }
 
index 445b270..bb5b7f1 100644 (file)
@@ -389,7 +389,7 @@ class IntermediateCommitSet {
         return Promise.all(fetchingPromises);
     }
 
-    commitsWithTestabilityWarnings() { return this.commits().filter((commit) => !!commit.testabilityWarning()); }
+    commitsWithTestability() { return this.commits().filter((commit) => !!commit.testability()); }
     commits() { return  Array.from(this._commitByRepository.values()); }
 
     _fetchCommitLogAndOwnedCommits(repository, revision)