Merge branch 'master' into stable-3.5 * master: TaskTree: remove 'ERROR:' prefix from log statement TaskTree: fix errorprone warning about Flogger Most development has happened in the stable-3.5 branch, but master has a couple commits missing from there. Merge them in before creating new stable branches based on stable-3.5. Change-Id: I0698db5f5cdadb5b208fdac09e0a4ffece20b2d0
diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.bazelignore
@@ -0,0 +1 @@ +node_modules
diff --git a/.bazelrc b/.bazelrc index 3ae03ff..fb8e4d1 100644 --- a/.bazelrc +++ b/.bazelrc
@@ -1,2 +1,18 @@ -build --workspace_status_command="python ./tools/workspace_status.py" +build --java_language_version=11 +build --java_runtime_version=remotejdk_11 +build --tool_java_language_version=11 +build --tool_java_runtime_version=remotejdk_11 + +build --workspace_status_command="python3 ./tools/workspace_status.py" +build --repository_cache=~/.gerritcodereview/bazel-cache/repository +build --action_env=PATH +build --disk_cache=~/.gerritcodereview/bazel-cache/cas + +# Enable strict_action_env flag to. For more information on this feature see +# https://groups.google.com/forum/#!topic/bazel-discuss/_VmRfMyyHBk. +# This will be the new default behavior at some point (and the flag was flipped +# shortly in 0.21.0 - https://github.com/bazelbuild/bazel/issues/7026). Remove +# this flag here once flipped in Bazel again. +build --incompatible_strict_action_env + test --build_tests_only
diff --git a/.bazelversion b/.bazelversion index fcdb2e1..c7cb131 100644 --- a/.bazelversion +++ b/.bazelversion
@@ -1 +1 @@ -4.0.0 +5.3.1
diff --git a/.eslintrc.json b/.eslintrc.json index 0c290fa..358c95d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json
@@ -4,7 +4,7 @@ "google" ], "parserOptions": { - "ecmaVersion": 8, + "ecmaVersion": 2020, "sourceType": "module" }, "env": {
diff --git a/.zuul.yaml b/.zuul.yaml index 27c8491..6e83fbf 100644 --- a/.zuul.yaml +++ b/.zuul.yaml
@@ -2,7 +2,9 @@ name: plugins-task-build parent: gerrit-plugin-build pre-run: - tools/playbooks/install_docker.yaml + - tools/playbooks/install_maven.yaml + - tools/playbooks/install_docker.yaml + - tools/playbooks/install_python3-distutils.yaml vars: bazelisk_test_targets: "plugins/task/lint_test plugins/task/..."
diff --git a/BUILD b/BUILD index 0b04753..537b627 100644 --- a/BUILD +++ b/BUILD
@@ -1,8 +1,48 @@ -load("//tools/bzl:plugin.bzl", "gerrit_plugin") +load( + "//tools/bzl:plugin.bzl", + "PLUGIN_DEPS", + "PLUGIN_TEST_DEPS", + "gerrit_plugin", +) load("//tools/bzl:js.bzl", "gerrit_js_bundle") load("//tools/js:eslint.bzl", "eslint") +load("//tools/bzl:junit.bzl", "junit_tests") +load("@rules_java//java:defs.bzl", "java_library", "java_plugin") +load("@rules_antlr//antlr:antlr4.bzl", "antlr") + plugin_name = "task" +java_plugin( + name = "auto-value-plugin", + processor_class = "com.google.auto.value.processor.AutoValueProcessor", + deps = [ + "@auto-value-annotations//jar", + "@auto-value//jar", + ], +) + +java_library( + name = "auto-value", + exported_plugins = [ + ":auto-value-plugin", + ], + visibility = ["//visibility:public"], + exports = ["@auto-value//jar"], +) + +antlr( + name = "task_reference", + srcs = ["src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4"], + package = "com.googlesource.gerrit.plugins.task", + visibility = ["//visibility:public"], +) + +java_library( + name = "task_reference_parser", + srcs = [":task_reference"], + deps = ["@antlr4_runtime//jar"], +) + gerrit_plugin( name = plugin_name, srcs = glob(["src/main/java/**/*.java"]), @@ -14,6 +54,11 @@ ], resource_jars = [":gr-task-plugin"], resources = glob(["src/main/resources/**/*"]), + deps = [ + ":auto-value", + ":task_reference_parser", + "@antlr4_runtime//jar", + ], javacopts = [ "-Werror", "-Xlint:all", "-Xlint:-classfile", "-Xlint:-processing"], ) @@ -23,6 +68,13 @@ entry_point = "gr-task-plugin/plugin.js", ) +junit_tests( + name = "junit-tests", + size = "small", + srcs = glob(["src/test/java/**/*Test.java"]), + deps = PLUGIN_TEST_DEPS + PLUGIN_DEPS + [plugin_name], +) + sh_test( name = "docker-tests", size = "medium", @@ -30,6 +82,7 @@ args = ["--task-plugin-jar", "$(location :task)"], data = [plugin_name] + glob(["test/**"]) + glob(["src/main/resources/Documentation/*"]), local = True, + tags = ["docker"], ) eslint( @@ -43,10 +96,5 @@ ".js", ], ignore = ".eslintignore", - plugins = [ - "@npm//eslint-config-google", - "@npm//eslint-plugin-html", - "@npm//eslint-plugin-import", - "@npm//eslint-plugin-jsdoc", - ], + plugins = [], )
diff --git a/WORKSPACE b/WORKSPACE index c217ecf..0cab5f6 100644 --- a/WORKSPACE +++ b/WORKSPACE
@@ -1,14 +1,12 @@ workspace( name = "task", - managed_directories = { - "@npm": ["node_modules"], - }, ) load("//:bazlets.bzl", "load_bazlets") load_bazlets( - commit = "6ebb3cfa1332a0dc0d2b7ea904a4703656f2ba54", + commit = "b6120a9fa50945d38f0a4d55d5879e3ec465c5e5", + shallow_since = "1701477032 -0700", #local_path = "/home/<user>/projects/bazlets", ) @@ -20,12 +18,23 @@ gerrit_polymer() -load("@build_bazel_rules_nodejs//:index.bzl", "yarn_install") +load("@build_bazel_rules_nodejs//:repositories.bzl", "build_bazel_rules_nodejs_dependencies") + +build_bazel_rules_nodejs_dependencies() + +load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install") + +node_repositories( + node_version = "16.13.2", + yarn_version = "1.22.17", +) yarn_install( name = "npm", + exports_directories_only = False, frozen_lockfile = False, package_json = "//:package.json", + symlink_node_modules = True, yarn_lock = "//:yarn.lock", ) @@ -35,4 +44,9 @@ "gerrit_api", ) +# Release Plugin API gerrit_api() + +load("//:external_plugin_deps.bzl", "external_plugin_deps") + +external_plugin_deps()
diff --git a/bazlets.bzl b/bazlets.bzl index f089af4..457bfec 100644 --- a/bazlets.bzl +++ b/bazlets.bzl
@@ -4,12 +4,14 @@ def load_bazlets( commit, + shallow_since = None, local_path = None): if not local_path: git_repository( name = NAME, remote = "https://gerrit.googlesource.com/bazlets", commit = commit, + shallow_since = shallow_since, ) else: native.local_repository(
diff --git a/external_plugin_deps.bzl b/external_plugin_deps.bzl new file mode 100644 index 0000000..901cd0f --- /dev/null +++ b/external_plugin_deps.bzl
@@ -0,0 +1,56 @@ +load("@bazel_tools//tools/build_defs/repo:maven_rules.bzl", "maven_jar") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +def external_plugin_deps(): + AUTO_VALUE_VERSION = "1.7.4" + + maven_jar( + name = "auto-value", + artifact = "com.google.auto.value:auto-value:" + AUTO_VALUE_VERSION, + sha1 = "6b126cb218af768339e4d6e95a9b0ae41f74e73d", + ) + + maven_jar( + name = "auto-value-annotations", + artifact = "com.google.auto.value:auto-value-annotations:" + AUTO_VALUE_VERSION, + sha1 = "eff48ed53995db2dadf0456426cc1f8700136f86", + ) + + http_archive( + name = "rules_antlr", + sha256 = "26e6a83c665cf6c1093b628b3a749071322f0f70305d12ede30909695ed85591", + strip_prefix = "rules_antlr-0.5.0", + urls = ["https://github.com/marcohu/rules_antlr/archive/0.5.0.tar.gz"], + ) + + maven_jar( + name = "antlr3_runtime", + artifact = "org.antlr:antlr-runtime:3.5.2", + sha1 = "cd9cd41361c155f3af0f653009dcecb08d8b4afd", + ) + + ANTLR_VERSION = "4.9.3" + + maven_jar( + name = "antlr4_runtime", + artifact = "org.antlr:antlr4-runtime:" + ANTLR_VERSION, + sha1 = "81befc16ebedb8b8aea3e4c0835dd5ca7e8523a8", + ) + + maven_jar( + name = "antlr4_tool", + artifact = "org.antlr:antlr4:" + ANTLR_VERSION, + sha1 = "9d47afaa75d70903b5b77413b034d6b201d7d5d6", + ) + + maven_jar( + name = "stringtemplate4", + artifact = "org.antlr:ST4:4.3.1", + sha1 = "9c61ac6d17b7f450b4048742c2cc73787972518e", + ) + + maven_jar( + name = "javax_json", + artifact = "org.glassfish:javax.json:1.0.4", + sha1 = "3178f73569fd7a1e5ffc464e680f7a8cc784b85a", + )
diff --git a/gr-task-plugin/gr-task-chip.js b/gr-task-plugin/gr-task-chip.js new file mode 100644 index 0000000..8e46bd0 --- /dev/null +++ b/gr-task-plugin/gr-task-chip.js
@@ -0,0 +1,66 @@ +/** + * @license + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import './gr-task-plugin.js'; +import {htmlTemplate} from './gr-task-chip_html.js'; + +class GrTaskChip extends Polymer.Element { + static get is() { + return 'gr-task-chip'; + } + + static get template() { + return htmlTemplate; + } + + static get properties() { + return { + chip_style: { + type: String, + notify: true, + value: 'ready', + }, + }; + } + + _setTasksTabActive() { + // TODO: Identify a better way as current implementation is fragile + const endPointDecorators = document.querySelector('gr-app') + .shadowRoot.querySelector('gr-app-element') + .shadowRoot.querySelector('main') + .querySelector('gr-change-view') + .shadowRoot.querySelector('#mainContent') + .getElementsByTagName('gr-endpoint-decorator'); + if (endPointDecorators) { + for (let i = 0; i <= endPointDecorators.length; i++) { + const el = endPointDecorators[i] + ?.shadowRoot?.querySelector('gr-task-plugin'); + if (el) { + el.shadowRoot.querySelector('paper-tabs') + .querySelector('paper-tab').scrollIntoView(); + break; + } + } + } + } + + _onChipClick() { + this._setTasksTabActive(); + } +} + +customElements.define(GrTaskChip.is, GrTaskChip); \ No newline at end of file
diff --git a/gr-task-plugin/gr-task-chip_html.js b/gr-task-plugin/gr-task-chip_html.js new file mode 100644 index 0000000..27cee0f --- /dev/null +++ b/gr-task-plugin/gr-task-chip_html.js
@@ -0,0 +1,129 @@ +/** + * @license + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const htmlTemplate = Polymer.html` + <style include="gr-a11y-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="shared-styles"> + .taskSummaryChip { + color: var(--chip-color); + cursor: pointer; + display: inline-block; + padding: var(--spacing-xxs) var(--spacing-m) var(--spacing-xxs) + var(--spacing-s); + margin-right: var(--spacing-s); + border-radius: 12px; + border: 1px solid gray; + vertical-align: top; + /* centered position of 20px chips in 24px line-height inline flow */ + vertical-align: top; + position: relative; + top: 2px; + } + .taskSummaryChip.loading { + border-color: var(--gray-foreground); + background: var(--gray-background); + } + .taskSummaryChip.loading:hover { + background: var(--gray-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.loading:focus-within { + background: var(--gray-background-focus); + } + .taskSummaryChip.success { + border-color: var(--success-foreground); + background: var(--success-background); + } + .taskSummaryChip.success:hover { + background: var(--success-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.success:focus-within { + background: var(--success-background-focus); + } + .taskSummaryChip.waiting { + border-color: var(--warning-foreground); + background: var(--warning-background); + } + .taskSummaryChip.waiting:hover { + background: var(--warning-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.waiting:focus-within { + background: var(--warning-background-focus); + } + .taskSummaryChip.ready { + border-color: var(--success-foreground); + background: var(--success-background); + } + .taskSummaryChip.ready:hover { + background: var(--success-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.ready:focus-within { + background: var(--success-background-focus); + } + .taskSummaryChip.invalid { + color: var(--error-foreground); + border-color: var(--error-foreground); + background: var(--error-background); + } + .taskSummaryChip.invalid:hover { + background: var(--error-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.invalid:focus-within { + background: var(--error-background-focus); + } + .taskSummaryChip.duplicate { + color: var(--success-foreground); + border-color: var(--success-foreground); + background: var(--success-background); + } + .taskSummaryChip.duplicate:hover { + background: var(--success-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.duplicate:focus-within { + background: var(--success-background-focus); + } + .taskSummaryChip.fail { + color: var(--error-foreground); + border-color: var(--error-foreground); + background: var(--error-background); + } + .taskSummaryChip.fail:hover { + background: var(--error-background-hover); + box-shadow: var(--elevation-level-1); + } + .taskSummaryChip.fail:focus-within { + background: var(--error-background-focus); + } + .font-small { + font-size: var(--font-size-small); + font-weight: var(--font-weight-normal); + line-height: var(--line-height-small); + } + </style> + <button + class$="taskSummaryChip font-small [[chip_style]]" + on-click="_onChipClick"> + <slot></slot> + </button> +`;
diff --git a/gr-task-plugin/gr-task-plugin-tasks.js b/gr-task-plugin/gr-task-plugin-tasks.js index 54585d1..7653ed4 100644 --- a/gr-task-plugin/gr-task-plugin-tasks.js +++ b/gr-task-plugin/gr-task-plugin-tasks.js
@@ -38,12 +38,21 @@ type: String, notify: true, }, + + config: { + type: Object, + value() { return {}; }, + }, }; } _can_show(show, task) { return show === 'true' || task.showOnFilter; } + + _getChangeUrl(change) { + return '/c/' + change.toString(); + } } customElements.define(GrTaskPluginTasks.is, GrTaskPluginTasks);
diff --git a/gr-task-plugin/gr-task-plugin-tasks_html.js b/gr-task-plugin/gr-task-plugin-tasks_html.js index df1644f..60fbd32 100644 --- a/gr-task-plugin/gr-task-plugin-tasks_html.js +++ b/gr-task-plugin/gr-task-plugin-tasks_html.js
@@ -20,12 +20,20 @@ <template is="dom-if" if="[[_can_show(show_all, task)]]"> <li> <style> - /* Matching colors with core code. */ + /* Matching colors with core theme. */ .green { - color: #9fcc6b; + color: var(--success-foreground); } .red { - color: #FFA62F; + color: var(--error-foreground); + } + .orange { + color: var(--warning-foreground); + } + .links { + color: var(--link-color); + cursor: pointer; + text-decoration: underline; } </style> <template is="dom-if" if="[[task.icon.id]]"> @@ -47,7 +55,21 @@ </iron-icon> </gr-tooltip-content> </template> - [[task.message]] + <template is="dom-if" if="[[task.change]]"> + <a class="links" href$="[[_getChangeUrl(task.change)]]">[[task.change]]</a> + </template> + <template is="dom-if" if="[[!task.change]]"> + <template is="dom-if" if="[[!task.hint]]"> + [[task.name]] + </template> + </template> + <template is="dom-if" if="[[task.hint]]"> + <gr-linked-text style="display: -webkit-inline-box;" + pre="" + content="[[task.hint]]" + config="[[config]]"> + </gr-linked-text> + </template> </li> </template> <ul style="list-style-type:none; margin: 0 0 0 0; padding: 0 0 0 2em;">
diff --git a/gr-task-plugin/gr-task-plugin.js b/gr-task-plugin/gr-task-plugin.js index c665d61..2ac355d 100644 --- a/gr-task-plugin/gr-task-plugin.js +++ b/gr-task-plugin/gr-task-plugin.js
@@ -22,10 +22,10 @@ const Defs = {}; /** * @typedef {{ - * message: string, * sub_tasks: Array<Defs.Task>, * hint: ?string, * name: string, + * change: ?number, * status: string * }} Defs.Task */ @@ -82,6 +82,30 @@ notify: true, value: 0, }, + _invalid_count: { + type: Number, + notify: true, + value: 0, + }, + _waiting_count: { + type: Number, + notify: true, + value: 0, + }, + _duplicate_count: { + type: Number, + notify: true, + value: 0, + }, + _pass_count: { + type: Number, + notify: true, + value: 0, + }, + _isPending: { + type: Boolean, + value: true, + }, }; } @@ -95,15 +119,21 @@ this._getTasks(); } + _is_hidden(_isPending, _tasks) { + return (!_isPending && !_tasks.length); + } + _getTasks() { if (!this.change) { return; } + this._isPending = true; const endpoint = `/changes/?q=change:${this.change._number}&--task--applicable`; return this.plugin.restApi().get(endpoint).then(response => { + this._isPending = false; if (response && response.length === 1) { const cinfo = response[0]; if (cinfo.plugins) { @@ -114,7 +144,20 @@ this._tasks = this._addTasks(taskPluginInfo.roots); } } + document.dispatchEvent(new CustomEvent('tasks-loaded', { + detail: { + ready_count: this._ready_count, + fail_count: this._fail_count, + invalid_count: this._invalid_count, + waiting_count: this._waiting_count, + duplicate_count: this._duplicate_count, + pass_count: this._pass_count, + }, + composed: true, bubbles: true, + })); } + }).catch(e => { + this._isPending = false; }); } @@ -127,7 +170,7 @@ icon.tooltip = 'Failed'; break; case 'READY': - icon.id = 'gr-icons:rebase'; + icon.id = 'gr-icons:playArrow'; icon.color = 'green'; icon.tooltip = 'Ready'; break; @@ -137,13 +180,18 @@ icon.tooltip = 'Invalid'; break; case 'WAITING': - icon.id = 'gr-icons:side-by-side'; - icon.color = 'red'; + icon.id = 'gr-icons:pause'; + icon.color = 'orange'; icon.tooltip = 'Waiting'; break; - case 'PASS': + case 'DUPLICATE': icon.id = 'gr-icons:check'; icon.color = 'green'; + icon.tooltip = 'Duplicate'; + break; + case 'PASS': + icon.id = 'gr-icons:check-circle'; + icon.color = 'green'; icon.tooltip = 'Passed'; break; } @@ -160,10 +208,10 @@ return false; } - _computeShowOnNeedsAndBlockedFilter(task) { + _computeShowOnNeededAndBlockedFilter(task) { return this._isFailOrReadyOrInvalid(task) || (task.sub_tasks && task.sub_tasks.some(t => - this._computeShowOnNeedsAndBlockedFilter(t))); + this._computeShowOnNeededAndBlockedFilter(t))); } _compute_counts(task) { @@ -175,15 +223,26 @@ case 'READY': this._ready_count++; break; + case 'INVALID': + this._invalid_count++; + break; + case 'WAITING': + this._waiting_count++; + break; + case 'DUPLICATE': + this._duplicate_count++; + break; + case 'PASS': + this._pass_count++; + break; } } _addTasks(tasks) { // rename to process, remove DOM bits if (!tasks) return []; tasks.forEach(task => { - task.message = task.hint || task.name; task.icon = this._computeIcon(task); - task.showOnFilter = this._computeShowOnNeedsAndBlockedFilter(task); + task.showOnFilter = this._computeShowOnNeededAndBlockedFilter(task); this._compute_counts(task); this._addTasks(task.sub_tasks); }); @@ -195,7 +254,7 @@ this._expand_all = 'true'; } - _needs_and_blocked_tap() { + _needed_and_blocked_tap() { this._show_all = 'false'; this._expand_all = 'true'; } @@ -203,6 +262,14 @@ _switch_expand() { this._expand_all = !this._expand_all; } + + _computeShowAllLabelText(showAllSections) { + if (showAllSections) { + return 'Hide all'; + } else { + return 'Show all'; + } + } } customElements.define(GrTaskPlugin.is, GrTaskPlugin);
diff --git a/gr-task-plugin/gr-task-plugin_html.js b/gr-task-plugin/gr-task-plugin_html.js index 71c995c..6ab5fb9 100644 --- a/gr-task-plugin/gr-task-plugin_html.js +++ b/gr-task-plugin/gr-task-plugin_html.js
@@ -16,65 +16,112 @@ */ export const htmlTemplate = Polymer.html` -<style> - ul { - padding-left: 0.5em; - margin-top: 0; - } - h3 { - padding-left: 0.1em; - margin: 0 0 0 0; - } - .cursor { cursor: pointer; } - .links { - color: blue; - cursor: pointer; - text-decoration: underline; - } - #tasks_header { - align-items: center; - background-color: #fafafa; - border-top: 1px solid #ddd; - display: flex; - padding: 6px 1rem; - } - .no-margins { margin: 0 0 0 0; } + <style include="gr-a11y-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="shared-styles"> + .header { + align-items: center; + background-color: var(--background-color-primary); + border-bottom: 1px solid var(--border-color); + display: flex; + padding: var(--spacing-s) var(--spacing-l); + z-index: 99; /* Less than gr-overlay's backdrop */ + } + .headerTitle { + align-items: center; + display: flex; + flex: 1; + } + .headerSubject { + font-family: var(--header-font-family); + font-size: var(--font-size-h3); + font-weight: var(--font-weight-h3); + line-height: var(--line-height-h3); + margin-left: var(--spacing-l); + } + paper-tabs { + background-color: var(--background-color-tertiary); + margin-top: var(--spacing-m); + height: calc(var(--line-height-h3) + var(--spacing-m)); + --paper-tabs-selection-bar-color: var(--link-color); + } + paper-tab { + box-sizing: border-box; + max-width: 12em; + --paper-tab-ink: var(--link-color); + } + section { + background-color: var(--view-background-color); + box-shadow: var(--elevation-level-1); + } + ul { + padding-left: 0.5em; + margin-top: 0; + } + .links { + color: var(--link-color); + cursor: pointer; + text-decoration: underline; + } + .show-all-button iron-icon { + color: inherit; + --iron-icon-height: 18px; + --iron-icon-width: 18px; + } + .no-margins { margin: 0 0 0 0; } + .task-list-item { + display: flex; + align-items: center; + column-gap: 1em; + padding-top: 12px; + padding-left: 12px; + } </style> -<div id="tasks" hidden$="[[!_tasks.length]]"> - <div id="tasks_header" style="display: flex;"> - <iron-icon - icon="gr-icons:expand-less" - hidden$="[[!_expand_all]]" - on-tap="_switch_expand" - class="cursor"> </iron-icon> - <iron-icon - icon="gr-icons:expand-more" - hidden$="[[_expand_all]]" - on-tap="_switch_expand" - class="cursor"> </iron-icon> - <div style="display: flex; align-items: center; column-gap: 1em;"> - <h3 class="no-margins" on-tap="_switch_expand" class="cursor"> Tasks </h3> +<div id="tasks" hidden$="[[_is_hidden(_isPending, _tasks)]]"> + <paper-tabs id="secondaryTabs" selected="0"> + <paper-tab + data-name$="Tasks" + class="Tasks" + > + Tasks + </paper-tab> + </paper-tabs> + <section class="TasksList"> + <div hidden$="[[!_isPending]]" class="task-list-item">Loading...</div> + <div hidden$="[[_isPending]]" class="task-list-item"> <template is="dom-if" if="[[_is_show_all(_show_all)]]"> - <p class="no-margins" >All ([[_all_count]]) | + <p> All ([[_all_count]]) | <span - on-click="_needs_and_blocked_tap" - class="links">Needs + Blocked ([[_ready_count]], [[_fail_count]])</span> + on-click="_needed_and_blocked_tap" + class="links">Needed + Blocked ([[_ready_count]], [[_fail_count]])</span> <p> </template> <template is="dom-if" if="[[!_is_show_all(_show_all)]]"> - <p class="no-margins" > <span + <p> <span class="links" on-click="_show_all_tap">All ([[_all_count]])</span> - | Needs + Blocked ([[_ready_count]], [[_fail_count]])</p> + | Needed + Blocked ([[_ready_count]], [[_fail_count]])</p> </template> + <gr-button link="" class="show-all-button" on-click="_switch_expand" + >[[_computeShowAllLabelText(_expand_all)]] + <iron-icon + icon="gr-icons:expand-more" + hidden$="[[_expand_all]]" + ></iron-icon + ><iron-icon + icon="gr-icons:expand-less" + hidden$="[[!_expand_all]]" + ></iron-icon> + </gr-button> </div> - </div> - <div hidden$="[[!_expand_all]]"> + <div hidden$="[[!_expand_all]]" style="padding-bottom: 12px"> <ul style="list-style-type:none;"> <gr-task-plugin-tasks tasks="[[_tasks]]" show_all$="[[_show_all]]"> </gr-task-plugin-tasks> </ul> </div> + </section> </div>`;
diff --git a/gr-task-plugin/gr-task-summary.js b/gr-task-plugin/gr-task-summary.js new file mode 100644 index 0000000..49c858d --- /dev/null +++ b/gr-task-plugin/gr-task-summary.js
@@ -0,0 +1,101 @@ +/** + * @license + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import {htmlTemplate} from './gr-task-summary_html.js'; +import './gr-task-chip.js'; + +class GrTaskSummary extends Polymer.Element { + static get is() { + return 'gr-task-summary'; + } + + static get template() { + return htmlTemplate; + } + + static get properties() { + return { + ready_count: { + type: Number, + notify: true, + value: 0, + }, + + fail_count: { + type: Number, + notify: true, + value: 0, + }, + + invalid_count: { + type: Number, + notify: true, + value: 0, + }, + + waiting_count: { + type: Number, + notify: true, + value: 0, + }, + + duplicate_count: { + type: Number, + notify: true, + value: 0, + }, + + pass_count: { + type: Number, + notify: true, + value: 0, + }, + + is_loading: { + type: Boolean, + value: true, + }, + }; + } + + /** @override */ + ready() { + super.ready(); + document.addEventListener('tasks-loaded', e => { + this.ready_count = e.detail.ready_count; + this.fail_count = e.detail.fail_count; + this.invalid_count = e.detail.invalid_count; + this.waiting_count = e.detail.waiting_count; + this.duplicate_count = e.detail.duplicate_count; + this.pass_count = e.detail.pass_count; + this.is_loading = false; + }); + } + + _can_show_summary(is_loading, ready_count, + fail_count, invalid_count, + waiting_count, duplicate_count, + pass_count) { + if (is_loading || ready_count || fail_count || invalid_count || + waiting_count || duplicate_count || pass_count) { + return true; + } + return false; + } +} + +customElements.define(GrTaskSummary.is, GrTaskSummary); \ No newline at end of file
diff --git a/gr-task-plugin/gr-task-summary_html.js b/gr-task-plugin/gr-task-summary_html.js new file mode 100644 index 0000000..4667060 --- /dev/null +++ b/gr-task-plugin/gr-task-summary_html.js
@@ -0,0 +1,87 @@ +/** + * @license + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const htmlTemplate = Polymer.html` + <style include="gr-a11y-styles"> + /* Workaround for empty style block - see https://github.com/Polymer/tools/issues/408 */ + </style> + <style include="shared-styles"> + :host { + display: block; + color: var(--deemphasized-text-color); + max-width: 625px; + margin-bottom: var(--spacing-m); + } + .zeroState { + color: var(--deemphasized-text-color); + } + .loading.zeroState { + margin-right: var(--spacing-m); + } + div.error, + .login { + display: flex; + color: var(--primary-text-color); + padding: 0 var(--spacing-s); + margin: var(--spacing-xs) 0; + width: 490px; + } + div.error { + background-color: var(--error-background); + } + div.error .right { + overflow: hidden; + } + div.error .right .message { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + td.key { + padding-right: var(--spacing-l); + padding-bottom: var(--spacing-s); + line-height: calc(var(--line-height-normal) + var(--spacing-s)); + } + td.value { + padding-right: var(--spacing-l); + padding-bottom: var(--spacing-s); + line-height: calc(var(--line-height-normal) + var(--spacing-s)); + display: flex; + } + div { + margin-left: var(--spacing-m); + } + </style> + <div class="task_summary"> + <table> + <tr> + <template is="dom-if" if="[[_can_show_summary(is_loading, ready_count, fail_count, invalid_count, waiting_count, duplicate_count, pass_count)]]"> + <td class="key">Tasks</td> + <td class="value"> + <gr-task-chip chip_style="loading" hidden$="[[!is_loading]]">loading...</gr-task-chip> + <gr-task-chip chip_style="fail" hidden$="[[!fail_count]]">[[fail_count]] blocked</gr-task-chip> + <gr-task-chip chip_style="invalid" hidden$="[[!invalid_count]]">[[invalid_count]] invalid</gr-task-chip> + <gr-task-chip chip_style="waiting" hidden$="[[!waiting_count]]">[[waiting_count]] waiting</gr-task-chip> + <gr-task-chip chip_style="ready" hidden$="[[!ready_count]]">[[ready_count]] needed</gr-task-chip> + <gr-task-chip chip_style="success" hidden$="[[!pass_count]]">[[pass_count]] passed</gr-task-chip> + <gr-task-chip chip_style="duplicate" hidden$="[[!duplicate_count]]">[[duplicate_count]] duplicate</gr-task-chip> + </template> + </td> + </tr> + </table> + </div> +`;
diff --git a/gr-task-plugin/plugin.js b/gr-task-plugin/plugin.js index 59d05b0..2219f12 100644 --- a/gr-task-plugin/plugin.js +++ b/gr-task-plugin/plugin.js
@@ -16,8 +16,11 @@ */ import './gr-task-plugin.js'; +import './gr-task-summary.js'; Gerrit.install(plugin => { plugin.registerCustomComponent( 'change-view-integration', 'gr-task-plugin'); + plugin.registerCustomComponent( + 'commit-container', 'gr-task-summary'); });
diff --git a/package.json b/package.json index fac70e5..82c0e23 100644 --- a/package.json +++ b/package.json
@@ -1,10 +1,10 @@ { "name": "task", - "version": "3.2.6-SNAPSHOT", + "version": "3.5.0-SNAPSHOT", "description": "Task Plugin", "dependencies": { - "@bazel/rollup": "^3.4.0", - "@bazel/terser": "^3.4.0" + "@bazel/rollup": "~5.1.0", + "@bazel/terser": "~5.1.0" }, "devDependencies": { "eslint": "^7.24.0",
diff --git a/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4 new file mode 100644 index 0000000..3869643 --- /dev/null +++ b/src/main/antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4
@@ -0,0 +1,184 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * + * This file defines the grammar used for Task Reference + * + * TASK_REFERENCE = [ + * [ // TASK_FILE_PATH ] | + * [ @USERNAME [ TASK_FILE_PATH ] ] | + * [ %GROUP_NAME [ TASK_FILE_PATH ] ] | + * [ %%GROUP_UUID [ TASK_FILE_PATH ] ] | + * [ TASK_FILE_PATH ] + * ] '^' TASK_NAME + * + * Examples: + * + * file: All-Projects:refs/meta/config:task.config + * reference: foo.config^sample + * Implied task: + * file: All-Projects:refs/meta/config:task/foo.config task: sample + * + * file: All-Projects:refs/meta/config:task/dir/bar.config + * reference: /foo.config^sample + * Implied task: + * file: All-Projects:refs/meta/config:task/foo.config task: sample + * + * file: All-Projects:refs/meta/config:task/dir/bar.config + * reference: sub-dir/foo.config^sample + * Implied task: + * file: All-Projects:refs/meta/config:task/dir/sub-dir/foo.config task: sample + * + * file: All-Projects:refs/meta/config:task/dir/bar.config + * reference: ^sample + * Implied task: + * file: All-Projects:refs/meta/config:task.config task: sample + * + * file: Any projects, ref, file + * reference: @jim^sample + * Implied task: + * file: All-Users:refs/users/<jim>:task.config task: sample + * + * file: Any projects, ref, file + * reference: @jim/foo^simple + * Implied task: + * file: All-Users:refs/users/<jim>:task/foo^simple task: sample + * + * file: Any projects, ref, file + * reference: //foo.config^sample + * Implied task: + * file: All-Projects:refs/meta/config:task/foo task: sample + * + * file: Any projects, ref, file + * reference: //^simple + * Implied task: + * file: All-Projects:refs/meta/config:task.config task: sample + * + * Suppose a8341ade45d83e867c24a2d37f47b410cfdbea6d is the UUID of 'CI System Owners' group. + * file: Any projects, ref, file + * reference: %CI System Owners^sample + * Implied task: + * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config + * task: sample + * + * file: Any projects, ref, file + * reference: %CI System Owners/foo^simple + * Implied task: + * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task/foo^simple + * task: sample + * + * file: Any projects, ref, file + * reference: %%a8341ade45d83e867c24a2d37f47b410cfdbea6d^sample + * Implied task: + * file: All-Users:refs/groups/a8/a8341ade45d83e867c24a2d37f47b410cfdbea6d:task.config + * task: sample + * + */ + +grammar TaskReference; + +options { + language = Java; +} + +reference + : file_path? TASK + ; + +file_path + : ALL_PROJECTS_ROOT + | FWD_SLASH absolute TASK_DELIMETER + | user absolute? TASK_DELIMETER + | group_name absolute? TASK_DELIMETER + | group_uuid absolute? TASK_DELIMETER + | (absolute| relative)? TASK_DELIMETER + ; + +user + : '@' NAME + ; + +group_name + : '%' (NAME | NAME_WITH_SPACES) + ; + +group_uuid + : '%%' INTERNAL_GROUP_UUID + ; + +absolute + : FWD_SLASH relative + ; + +relative + : dir* NAME + ; + +dir + : (NAME FWD_SLASH) + ; + +TASK + : (~'^')+ EOF + ; + +INTERNAL_GROUP_UUID + : HEX_10 HEX_10 HEX_10 HEX_10 + ; + +fragment HEX_10 + : HEX HEX HEX HEX HEX HEX HEX HEX HEX HEX + ; + +fragment HEX + : [0-9a-f] + ; + +NAME + : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH* + ; + +NAME_WITH_SPACES + : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE (URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH | SPACE)* + ; + +fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH + : URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE + | '@' | '%' + ; + +fragment URL_ALLOWED_CHARS_EXCEPT_FWD_SLASH_AND_AT_AND_PERCENTILE + : ':' | '?' | '#' | '[' | ']' + |'!' | '$' | '&' | '\'' | '(' | ')' + | '*' | '+' | ',' | ';' | '=' + | 'A'..'Z' | 'a'..'z' | '0'..'9' + | '_' | '.' | '\\' | '-' | '~' + ; + +fragment SPACE + : ' ' + ; + +TASK_DELIMETER + : '^' + ; + +ALL_PROJECTS_ROOT + : FWD_SLASH FWD_SLASH TASK_DELIMETER + ; + +FWD_SLASH + : '/' + ;
diff --git a/src/main/java/com/google/gerrit/common/BooleanTable.java b/src/main/java/com/google/gerrit/common/BooleanTable.java new file mode 100644 index 0000000..9e89eb9 --- /dev/null +++ b/src/main/java/com/google/gerrit/common/BooleanTable.java
@@ -0,0 +1,69 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.common; + +import java.util.BitSet; +import java.util.HashMap; +import java.util.Map; + +/** + * A space efficient Table for Booleans. This Table takes advantage of the fact that the values + * stored in it are all Booleans and uses BitSets to make this very space efficient. + */ +public class BooleanTable<R, C> { + protected class Row { + public final BitSet hasValues = new BitSet(); + public final BitSet values = new BitSet(); + + public void setPosition(int position, Boolean value) { + if (value != null) { + values.set(position, value); + } + hasValues.set(position, value != null); + } + + public Boolean getPosition(int position) { + if (hasValues.get(position)) { + return values.get(position); + } + return null; + } + } + + protected Map<R, Row> rowByRow = new HashMap<>(); + protected Map<C, Integer> positionByColumn = new HashMap<>(); + protected int highestPosition = -1; + + public void put(R r, C c, Boolean v) { + Row row = rowByRow.computeIfAbsent(r, k -> new Row()); + Integer columnPosition = positionByColumn.computeIfAbsent(c, k -> nextPosition()); + row.setPosition(columnPosition, v); + } + + protected int nextPosition() { + return ++highestPosition; + } + + public Boolean get(R r, C c) { + Row row = rowByRow.get(r); + if (row != null) { + Integer columnPosition = positionByColumn.get(c); + if (columnPosition != null) { + return row.getPosition(columnPosition); + } + } + return null; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java new file mode 100644 index 0000000..d5a8ad1 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/ConfigSourcedValue.java
@@ -0,0 +1,32 @@ +// Copyright (C) 2024 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class ConfigSourcedValue { + public static ConfigSourcedValue create(FileKey sourceFile, String value) { + return new AutoValue_ConfigSourcedValue(sourceFile, value); + } + + public static Class<? extends ConfigSourcedValue> getClassType() { + return AutoValue_ConfigSourcedValue.class; + } + + public abstract FileKey sourceFile(); + + public abstract String value(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java new file mode 100644 index 0000000..9856075 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/FileKey.java
@@ -0,0 +1,35 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.auto.value.AutoValue; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.Project; + +/** An immutable reference to a fully qualified file in gerrit repo. */ +@AutoValue +public abstract class FileKey { + public static FileKey create(Project.NameKey project, String branch, String file) { + return new AutoValue_FileKey(BranchNameKey.create(project, branch), file); + } + + public static FileKey create(BranchNameKey branch, String file) { + return new AutoValue_FileKey(branch, file); + } + + public abstract BranchNameKey branch(); + + public abstract String file(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java new file mode 100644 index 0000000..472b1e7 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/IsTrueOperator.java
@@ -0,0 +1,61 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.gerrit.extensions.annotations.Exports; +import com.google.gerrit.index.query.Predicate; +import com.google.gerrit.index.query.QueryParseException; +import com.google.gerrit.server.query.change.ChangeData; +import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.query.change.SubmitRequirementPredicate; +import com.google.inject.AbstractModule; + +// TODO: Remove this class when up-merging to Gerrit v3.6+ as it supports +// the 'is:true' submit requirement +public class IsTrueOperator implements ChangeQueryBuilder.ChangeIsOperandFactory { + + public static final String TRUE = "true"; + + public static class Module extends AbstractModule { + @Override + protected void configure() { + bind(ChangeQueryBuilder.ChangeIsOperandFactory.class) + .annotatedWith(Exports.named(TRUE)) + .to(IsTrueOperator.class); + } + } + + public class TruePredicate extends SubmitRequirementPredicate { + + public TruePredicate() { + super("is", TRUE); + } + + @Override + public boolean match(ChangeData data) { + return true; + } + + @Override + public int getCost() { + return 1; + } + } + + @Override + public Predicate<ChangeData> create(ChangeQueryBuilder builder) throws QueryParseException { + return new IsTrueOperator.TruePredicate(); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java index 45fe46d..0be04ba 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/MatchCache.java
@@ -14,44 +14,54 @@ package com.googlesource.gerrit.plugins.task; +import com.google.gerrit.entities.Change; import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.index.query.Matchable; import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.server.query.change.ChangeData; -import java.util.HashMap; -import java.util.Map; +import com.googlesource.gerrit.plugins.task.statistics.HitBooleanTable; +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; public class MatchCache { + protected final HitBooleanTable<String, Change.Id> resultByChangeByQuery = + new HitBooleanTable<>(); protected final PredicateCache predicateCache; - protected final ChangeData changeData; - protected final Map<String, Boolean> matchResultByQuery = new HashMap<>(); - - public MatchCache(PredicateCache predicateCache, ChangeData changeData) { + public MatchCache(PredicateCache predicateCache) { this.predicateCache = predicateCache; - this.changeData = changeData; } - protected boolean match(String query) throws StorageException, QueryParseException { - if (query == null) { + public Boolean matchOrNull(ChangeData changeData, String query, boolean isVisible) { + try { + return match(changeData, query, isVisible); + } catch (StorageException | QueryParseException e) { + } + return null; + } + + @SuppressWarnings("try") + public boolean match(ChangeData changeData, String query, boolean isVisible) + throws StorageException, QueryParseException { + if (query == null || "true".equalsIgnoreCase(query)) { return true; } - Boolean isMatched = matchResultByQuery.get(query); + Boolean isMatched = resultByChangeByQuery.get(query, changeData.getId()); if (isMatched == null) { - isMatched = predicateCache.match(changeData, query); - matchResultByQuery.put(query, isMatched); + Matchable<ChangeData> matchable = predicateCache.getPredicate(query, isVisible).asMatchable(); + try (StopWatch stopWatch = + resultByChangeByQuery.createLoadingStopWatch(query, changeData.getId(), isVisible)) { + isMatched = matchable.match(changeData); + resultByChangeByQuery.put(query, changeData.getId(), isMatched); + } } return isMatched; } - protected Boolean matchOrNull(String query) { - if (query == null) { - return null; - } - Boolean isMatched = matchResultByQuery.get(query); - if (isMatched == null) { - isMatched = predicateCache.matchOrNull(changeData, query); - matchResultByQuery.put(query, isMatched); - } - return isMatched; + public void initStatistics(int summaryCount) { + resultByChangeByQuery.initStatistics(summaryCount); + } + + public Object getStatistics() { + return resultByChangeByQuery.getStatistics(); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java index 3a8d903..3451836 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/Modules.java
@@ -15,6 +15,8 @@ package com.googlesource.gerrit.plugins.task; import com.google.gerrit.extensions.annotations.Exports; +import com.google.gerrit.extensions.config.CapabilityDefinition; +import com.google.gerrit.extensions.config.FactoryModule; import com.google.gerrit.extensions.registration.DynamicSet; import com.google.gerrit.extensions.webui.JavaScriptPlugin; import com.google.gerrit.extensions.webui.WebUiPlugin; @@ -23,7 +25,6 @@ import com.google.gerrit.server.restapi.change.GetChange; import com.google.gerrit.server.restapi.change.QueryChanges; import com.google.gerrit.sshd.commands.Query; -import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument; import java.util.ArrayList; @@ -31,13 +32,24 @@ import org.kohsuke.args4j.Option; public class Modules { - public static class Module extends AbstractModule { + public static class Module extends FactoryModule { @Override protected void configure() { + bind(CapabilityDefinition.class) + .annotatedWith(Exports.named(ViewPathsCapability.VIEW_PATHS)) + .to(ViewPathsCapability.class); + factory(TaskPath.Factory.class); + factory(TaskReference.Factory.class); + factory(TaskExpression.Factory.class); + factory(TaskTree.Factory.class); + factory(Preloader.Factory.class); + bind(ChangePluginDefinedInfoFactory.class) .annotatedWith(Exports.named("task")) .to(TaskAttributeFactory.class); + install(new IsTrueOperator.Module()); + bind(DynamicBean.class).annotatedWith(Exports.named(GetChange.class)).to(MyOptions.class); bind(DynamicBean.class).annotatedWith(Exports.named(Query.class)).to(MyOptions.class); bind(DynamicBean.class).annotatedWith(Exports.named(QueryChanges.class)).to(MyOptions.class); @@ -47,6 +59,13 @@ } public static class MyOptions implements DynamicBean { + @Option( + name = "--only", + usage = + "Evaluate tasks under this task only. Only root task names are supported." + + " This option can be provided multiple times.") + public List<String> includedRoots = new ArrayList<>(); + @Option(name = "--all", usage = "Include all visible tasks in the output") public boolean all = false; @@ -58,9 +77,18 @@ usage = "Include only invalid tasks and the tasks referencing them in the output") public boolean onlyInvalid = false; + @Option(name = "--include-paths", usage = "Include absolute path to each task") + public boolean includePaths = false; + @Option(name = "--evaluation-time", usage = "Include elapsed evaluation time on each task") public boolean evaluationTime = false; + @Option(name = "--include-statistics", usage = "Include statistcs about the task evaluations") + public boolean includeStatistics = false; + + @Option(name = "--summary-count", usage = "number of items to output in statistics summaries") + public int summaryCount = 5; + @Option( name = "--preview", metaVar = "{CHANGE,PATCHSET}", @@ -78,5 +106,9 @@ public MyOptions(PatchSetArgument.Factory patchSetArgumentFactory) { this.patchSetArgumentFactory = patchSetArgumentFactory; } + + public boolean shouldFilterRoot(String rootName) { + return !includedRoots.isEmpty() && !includedRoots.contains(rootName); + } } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java index 7896417..268b1a3 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/PredicateCache.java
@@ -14,63 +14,81 @@ package com.googlesource.gerrit.plugins.task; -import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.extensions.annotations.PluginName; +import com.google.gerrit.index.FieldDef; +import com.google.gerrit.index.query.AndPredicate; +import com.google.gerrit.index.query.NotPredicate; +import com.google.gerrit.index.query.OrPredicate; import com.google.gerrit.index.query.Predicate; import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.config.GerritServerConfig; +import com.google.gerrit.server.index.change.ChangeField; import com.google.gerrit.server.query.change.ChangeData; -import com.google.gerrit.server.query.change.ChangeQueryBuilder; +import com.google.gerrit.server.query.change.ChangeIndexPredicate; +import com.google.gerrit.server.query.change.DestinationPredicate; +import com.google.gerrit.server.query.change.RegexProjectPredicate; +import com.google.gerrit.server.query.change.RegexRefPredicate; +import com.google.gerrit.server.query.change.SubmitRequirementChangeQueryBuilder; import com.google.inject.Inject; -import java.util.HashMap; -import java.util.Map; +import com.googlesource.gerrit.plugins.task.statistics.HitHashMap; +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; +import com.googlesource.gerrit.plugins.task.util.ThrowingProvider; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import org.eclipse.jgit.lib.Config; public class PredicateCache { - protected final ChangeQueryBuilder cqb; - protected final CurrentUser user; + public static class Statistics { + protected Object predicatesByQueryCache; + } - protected final Map<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>> - predicatesByQuery = new HashMap<>(); + protected final SubmitRequirementChangeQueryBuilder srcqb; + protected final Set<String> cacheableByBranchPredicateClassNames; + protected final CurrentUser user; + protected final HitHashMap<String, ThrowingProvider<Predicate<ChangeData>, QueryParseException>> + predicatesByQuery = new HitHashMap<>(); + + protected Statistics statistics; @Inject - public PredicateCache(CurrentUser user, ChangeQueryBuilder cqb) { + public PredicateCache( + @GerritServerConfig Config config, + @PluginName String pluginName, + CurrentUser user, + SubmitRequirementChangeQueryBuilder srcqb) { this.user = user; - this.cqb = cqb; + this.srcqb = srcqb; + cacheableByBranchPredicateClassNames = + new HashSet<>( + Arrays.asList( + config.getStringList(pluginName, "cacheable-predicates", "byBranch-className"))); } - public boolean match(ChangeData c, String query) throws StorageException, QueryParseException { - if (query == null) { - return true; + public void initStatistics(int summaryCount) { + statistics = new Statistics(); + predicatesByQuery.initStatistics(summaryCount); + } + + public Object getStatistics() { + if (statistics != null) { + statistics.predicatesByQueryCache = predicatesByQuery.getStatistics(); } - return matchWithExceptions(c, query); + return statistics; } - public Boolean matchOrNull(ChangeData c, String query) { - if (query != null) { - try { - return matchWithExceptions(c, query); - } catch (QueryParseException | RuntimeException e) { - } - } - return null; - } - - protected boolean matchWithExceptions(ChangeData c, String query) - throws QueryParseException, StorageException { - if ("true".equalsIgnoreCase(query)) { - return true; - } - return getPredicate(query).asMatchable().match(c); - } - - protected Predicate<ChangeData> getPredicate(String query) throws QueryParseException { + @SuppressWarnings("try") + public Predicate<ChangeData> getPredicate(String query, boolean isVisible) + throws QueryParseException { ThrowingProvider<Predicate<ChangeData>, QueryParseException> predProvider = predicatesByQuery.get(query); if (predProvider != null) { return predProvider.get(); } // never seen 'query' before - try { - Predicate<ChangeData> pred = cqb.parse(query); + try (StopWatch stopWatch = predicatesByQuery.createLoadingStopWatch(query, isVisible)) { + Predicate<ChangeData> pred = srcqb.parse(query); predicatesByQuery.put(query, new ThrowingProvider.Entry<>(pred)); return pred; } catch (QueryParseException e) { @@ -78,4 +96,43 @@ throw e; } } + + /** + * Can this query's output be assumed to be constant given any Change destined for the same + * Branch.NameKey? + */ + public boolean isCacheableByBranch(String query, boolean isVisible) throws QueryParseException { + if (query == null + || "".equals(query) + || "false".equalsIgnoreCase(query) + || "true".equalsIgnoreCase(query)) { + return true; + } + return isCacheableByBranch(getPredicate(query, isVisible)); + } + + protected boolean isCacheableByBranch(Predicate<ChangeData> predicate) { + if (predicate instanceof AndPredicate + || predicate instanceof NotPredicate + || predicate instanceof OrPredicate) { + for (Predicate<ChangeData> subPred : predicate.getChildren()) { + if (!isCacheableByBranch(subPred)) { + return false; + } + } + return true; + } + if (predicate instanceof DestinationPredicate + || predicate instanceof RegexProjectPredicate + || predicate instanceof RegexRefPredicate) { + return true; + } + if (predicate instanceof ChangeIndexPredicate) { + FieldDef<ChangeData, ?> field = ((ChangeIndexPredicate) predicate).getField(); + if (field.equals(ChangeField.PROJECT) || field.equals(ChangeField.REF)) { + return true; + } + } + return cacheableByBranchPredicateClassNames.contains(predicate.getClass().getName()); + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java index 8babe1c..7b325cf 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/Preloader.java
@@ -14,68 +14,181 @@ package com.googlesource.gerrit.plugins.task; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; import com.googlesource.gerrit.plugins.task.TaskConfig.Task; +import com.googlesource.gerrit.plugins.task.statistics.HitHashMap; +import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap; +import java.io.IOException; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; import org.eclipse.jgit.errors.ConfigInvalidException; /** Use to pre-load a task definition with values from its preload-task definition. */ public class Preloader { - public static void preload(Task definition) throws ConfigInvalidException { - String name = definition.preloadTask; - if (name != null) { - Task task = definition.config.getTaskOptional(name); - if (task != null) { - preload(task); - preloadFrom(definition, task); - } - } + public interface Factory { + Preloader create(@Assisted TaskConfigCache taskConfigCache); } - protected static void preloadFrom(Task definition, Task preloadFrom) { + public static class Statistics { + protected Object optionalTaskByExpressionCache; + protected long loaded; + protected long preloaded; + protected long preloadedFromDefinition; + } + + protected final TaskConfigCache taskConfigCache; + protected final TaskExpression.Factory taskExpressionFactory; + protected final StatisticsMap<TaskExpressionKey, Optional<Task>> optionalTaskByExpression = + new HitHashMap<>(); + + protected Statistics statistics; + + @Inject + public Preloader( + TaskExpression.Factory taskExpressionFactory, @Assisted TaskConfigCache taskConfigCache) { + this.taskConfigCache = taskConfigCache; + this.taskExpressionFactory = taskExpressionFactory; + } + + public List<Task> getRootTasks() throws IOException, ConfigInvalidException { + return getTasks(taskConfigCache.getRootConfig(), TaskConfig.SECTION_ROOT); + } + + public List<Task> getTasks(FileKey file) throws IOException, ConfigInvalidException { + return getTasks(taskConfigCache.getTaskConfig(file), TaskConfig.SECTION_TASK); + } + + protected List<Task> getTasks(TaskConfig cfg, String type) throws IOException { + List<Task> preloaded = new ArrayList<>(); + for (Task task : cfg.getTasks(type)) { + try { + preloaded.add(preload(task)); + } catch (ConfigInvalidException e) { + preloaded.add(null); + } + } + return preloaded; + } + + boolean inGetOptionalTask; + + /** + * Get a preloaded Task for this TaskExpression. + * + * @param expression + * @return Optional<Task> which is empty if the expression is optional and no tasks are resolved + * @throws ConfigInvalidException if the expression requires a task and no tasks are resolved + */ + public Optional<Task> getOptionalTask(TaskExpression expression) + throws ConfigInvalidException, IOException { + Optional<Task> task = optionalTaskByExpression.get(expression.key); + if (task == null) { + boolean firstInGetOptionalTask = !inGetOptionalTask; + inGetOptionalTask = true; + task = preloadOptionalTask(expression); + optionalTaskByExpression.put(expression.key, task); + if (firstInGetOptionalTask) { + inGetOptionalTask = false; + } + } + return task; + } + + protected Optional<Task> preloadOptionalTask(TaskExpression expression) + throws ConfigInvalidException, IOException { + Optional<Task> definition = loadOptionalTask(expression); + return definition.isPresent() ? Optional.of(preload(definition.get())) : definition; + } + + public Task preload(Task definition) throws ConfigInvalidException, IOException { + if (statistics != null && !inGetOptionalTask) { + statistics.preloadedFromDefinition++; + } + String expression = definition.preloadTask; + if (expression != null) { + if (statistics != null) { + statistics.preloaded++; + } + Optional<Task> preloadFrom = + getOptionalTask(taskExpressionFactory.create(definition.file(), expression)); + if (preloadFrom.isPresent()) { + return preloadFrom(definition, preloadFrom.get()); + } + } + return definition; + } + + protected Optional<Task> loadOptionalTask(TaskExpression expression) + throws ConfigInvalidException, IOException { + if (statistics != null) { + statistics.loaded++; + } + try { + for (TaskKey key : expression) { + Optional<Task> task = getOptionalTask(key); + if (task.isPresent()) { + return task; + } + } + } catch (RuntimeConfigInvalidException e) { + throw e.checkedException; + } catch (NoSuchElementException e) { + // expression was not optional but we ran out of names to try + throw new ConfigInvalidException("task not defined"); + } + return Optional.empty(); + } + + protected static Task preloadFrom(Task definition, Task preloadFrom) { + Task preloadTo = definition.config.new Task(definition.subSection); for (Field field : definition.getClass().getFields()) { String name = field.getName(); - if ("isVisible".equals(name) || "isTrusted".equals(name) || "config".equals(name)) { + if ("config".equals(name)) { continue; } try { field.setAccessible(true); - preloadField(field.getType(), field, definition, preloadFrom); + preloadField(field, definition, preloadFrom, preloadTo); } catch (IllegalAccessException | IllegalArgumentException e) { throw new RuntimeException(); } } + return preloadTo; } - protected static <T, S, K, V> void preloadField( - Class<T> clz, Field field, Task definition, Task preloadFrom) + protected Optional<Task> getOptionalTask(TaskKey key) throws IOException, ConfigInvalidException { + return taskConfigCache.getTaskConfig(key.subSection().file()).getOptionalTask(key.task()); + } + + protected static <S, K, V> void preloadField( + Field field, Task definition, Task preloadFrom, Task preloadTo) throws IllegalArgumentException, IllegalAccessException { - T pre = getField(clz, field, preloadFrom); - if (pre != null) { - T val = getField(clz, field, definition); - if (val == null) { - field.set(definition, pre); - } else if (val instanceof List) { - List<?> valList = List.class.cast(val); - List<?> preList = List.class.cast(pre); - field.set(definition, preloadListFrom(castUnchecked(valList), castUnchecked(preList))); - } else if (val instanceof Map) { - Map<?, ?> valMap = Map.class.cast(val); - Map<?, ?> preMap = Map.class.cast(pre); - field.set(definition, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap))); - } // nothing to do for overridden preloaded scalars + Object pre = field.get(preloadFrom); + Object val = field.get(definition); + if (val == null) { + field.set(preloadTo, pre); + } else if (pre == null) { + field.set(preloadTo, val); + } else if (val instanceof List) { + List<?> valList = List.class.cast(val); + List<?> preList = List.class.cast(pre); + field.set(preloadTo, preloadListFrom(castUnchecked(valList), castUnchecked(preList))); + } else if (val instanceof Map) { + Map<?, ?> valMap = Map.class.cast(val); + Map<?, ?> preMap = Map.class.cast(pre); + field.set(preloadTo, preloadMapFrom(castUnchecked(valMap), castUnchecked(preMap))); + } else { + field.set(preloadTo, val); } } - protected static <T> T getField(Class<T> clz, Field field, Object obj) - throws IllegalArgumentException, IllegalAccessException { - return clz.cast(field.get(obj)); - } - @SuppressWarnings("unchecked") protected static <S> List<S> castUnchecked(List<?> list) { List<S> forceCheck = (List<S>) list; @@ -89,28 +202,40 @@ } protected static <T> List<T> preloadListFrom(List<T> list, List<T> preList) { - List<T> extended = list; - if (!preList.isEmpty()) { - extended = preList; - if (!list.isEmpty()) { - extended = new ArrayList<>(list.size() + preList.size()); - extended.addAll(preList); - extended.addAll(list); - } + if (preList.isEmpty()) { + return list; } + if (list.isEmpty()) { + return preList; + } + + List<T> extended = new ArrayList<>(list.size() + preList.size()); + extended.addAll(preList); + extended.addAll(list); return extended; } protected static <K, V> Map<K, V> preloadMapFrom(Map<K, V> map, Map<K, V> preMap) { - Map<K, V> extended = map; - if (!preMap.isEmpty()) { - extended = preMap; - if (!map.isEmpty()) { - extended = new HashMap<>(map.size() + preMap.size()); - extended.putAll(preMap); - extended.putAll(map); - } + if (preMap.isEmpty()) { + return map; } + if (map.isEmpty()) { + return preMap; + } + + Map<K, V> extended = new HashMap<>(map.size() + preMap.size()); + extended.putAll(preMap); + extended.putAll(map); return extended; } + + public void initStatistics(int summaryCount) { + statistics = new Statistics(); + optionalTaskByExpression.initStatistics(summaryCount); + } + + public Statistics getStatistics() { + statistics.optionalTaskByExpressionCache = optionalTaskByExpression.getStatistics(); + return statistics; + } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java deleted file mode 100644 index 028762c..0000000 --- a/src/main/java/com/googlesource/gerrit/plugins/task/Properties.java +++ /dev/null
@@ -1,215 +0,0 @@ -// Copyright (C) 2019 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.googlesource.gerrit.plugins.task; - -import com.google.common.collect.Sets; -import com.google.gerrit.entities.Change; -import com.google.gerrit.exceptions.StorageException; -import com.google.gerrit.server.query.change.ChangeData; -import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory; -import com.googlesource.gerrit.plugins.task.TaskConfig.Task; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** Use to expand properties like ${_name} in the text of various definitions. */ -public class Properties { - /** Use to expand properties specifically for Tasks. */ - public static class Task extends Expander { - public static final Task EMPTY_PARENT = new Task(); - - public Task() { - super(Collections.emptyMap()); - } - - public Task(ChangeData changeData, TaskConfig.Task definition, Task parentProperties) - throws StorageException { - super(parentProperties.forDescendants()); - valueByName.putAll(getInternalProperties(definition, changeData)); - new RecursiveExpander(valueByName).expand(definition.getAllProperties()); - - definition.setExpandedProperties(valueByName); - - expandFieldValues(definition, Collections.emptySet()); - } - - protected Map<String, String> forDescendants() { - return new HashMap<>(valueByName); - } - } - - /** Use to expand properties specifically for NamesFactories. */ - public static class NamesFactory extends Expander { - public NamesFactory(TaskConfig.NamesFactory namesFactory, Task properties) { - super(properties.valueByName); - expandFieldValues(namesFactory, Sets.newHashSet(TaskConfig.KEY_TYPE)); - } - } - - protected static Map<String, String> getInternalProperties( - TaskConfig.Task definition, ChangeData changeData) throws StorageException { - Map<String, String> properties = new HashMap<>(); - - properties.put("_name", definition.name); - - Change c = changeData.change(); - properties.put("_change_number", String.valueOf(c.getId().get())); - properties.put("_change_id", c.getKey().get()); - properties.put("_change_project", c.getProject().get()); - properties.put("_change_branch", c.getDest().branch()); - properties.put("_change_status", c.getStatus().toString()); - properties.put("_change_topic", c.getTopic()); - - return properties; - } - - /** - * Use to expand properties whose values may contain other references to properties. - * - * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there - * are no looping definitions. - * - * <p>Given some property name/value asssociations defined like this: - * - * <p><code> - * valueByName.put("obstacle", "fence"); - * valueByName.put("action", "jumped over the ${obstacle}"); - * </code> - * - * <p>a String like: <code>"The brown fox ${action}."</code> - * - * <p>will expand to: <code>"The brown fox jumped over the fence."</code> - */ - protected static class RecursiveExpander { - protected final Expander expander; - protected Map<String, String> unexpandedByName; - protected Set<String> expanding; - - public RecursiveExpander(Map<String, String> valueByName) { - expander = - new Expander(valueByName) { - @Override - protected String getValueForName(String name) { - expandUnexpanded(name); // recursive call - return super.getValueForName(name); - } - }; - } - - public void expand(Map<String, String> unexpandedByName) { - this.unexpandedByName = unexpandedByName; - - // Copy keys to allow out of order removals during iteration - for (String unexpanedName : new ArrayList<>(unexpandedByName.keySet())) { - expanding = new HashSet<>(); - expandUnexpanded(unexpanedName); - } - } - - protected void expandUnexpanded(String name) { - if (!expanding.add(name)) { - throw new RuntimeException("Looping property definitions."); - } - String value = unexpandedByName.remove(name); - if (value != null) { - expander.valueByName.put(name, expander.expandText(value)); - } - } - } - - /** - * Use to expand properties like ${property} in Strings into their values. - * - * <p>Given some property name/value asssociations defined like this: - * - * <p><code> - * valueByName.put("animal", "fox"); - * valueByName.put("bar", "foo"); - * valueByName.put("obstacle", "fence"); - * </code> - * - * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code> - * - * <p>will expand to: <code>"The brown fox jumped over the fence."</code> - */ - protected static class Expander { - // "${_name}" -> group(1) = "_name" - protected static final Pattern PATTERN = Pattern.compile("\\$\\{([^}]+)\\}"); - - public final Map<String, String> valueByName; - - public Expander(Map<String, String> valueByName) { - this.valueByName = valueByName; - } - - /** Expand all properties in the Strings in the object's Fields (except the exclude ones) */ - protected void expandFieldValues(Object object, Set<String> excludedFieldNames) { - for (Field field : object.getClass().getFields()) { - try { - if (!excludedFieldNames.contains(field.getName())) { - field.setAccessible(true); - Object o = field.get(object); - if (o instanceof String) { - field.set(object, expandText((String) o)); - } else if (o instanceof List) { - @SuppressWarnings("unchecked") - List<String> forceCheck = List.class.cast(o); - expandElements(forceCheck); - } - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - - /** Expand all properties in the Strings in the List */ - public void expandElements(List<String> list) { - if (list != null) { - for (ListIterator<String> it = list.listIterator(); it.hasNext(); ) { - it.set(expandText(it.next())); - } - } - } - - /** Expand all properties (${property_name} -> property_value) in the given text */ - public String expandText(String text) { - if (text == null) { - return null; - } - StringBuffer out = new StringBuffer(); - Matcher m = PATTERN.matcher(text); - while (m.find()) { - m.appendReplacement(out, Matcher.quoteReplacement(getValueForName(m.group(1)))); - } - m.appendTail(out); - return out.toString(); - } - - /** Get the replacement value for the property identified by name */ - protected String getValueForName(String name) { - String value = valueByName.get(name); - return value == null ? "" : value; - } - } -}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java new file mode 100644 index 0000000..cc0ed94 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/RuntimeConfigInvalidException.java
@@ -0,0 +1,27 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import org.eclipse.jgit.errors.ConfigInvalidException; + +public class RuntimeConfigInvalidException extends RuntimeException { + protected static final long serialVersionUID = 1L; + protected ConfigInvalidException checkedException; + + public RuntimeConfigInvalidException(ConfigInvalidException e) { + super(e); + this.checkedException = e; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java new file mode 100644 index 0000000..54db1e4 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/SubSectionKey.java
@@ -0,0 +1,31 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.auto.value.AutoValue; + +/** An immutable reference to a SubSection in fully qualified task config file. */ +@AutoValue +public abstract class SubSectionKey { + public static SubSectionKey create(FileKey file, String section, String subSection) { + return new AutoValue_SubSectionKey(file, section, subSection == null ? "" : subSection); + } + + public abstract FileKey file(); + + public abstract String section(); + + public abstract String subSection(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java index 1e19f7b..ca1ae2c 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskAttributeFactory.java
@@ -14,13 +14,14 @@ package com.googlesource.gerrit.plugins.task; -import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Change; import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.extensions.api.access.PluginPermission; import com.google.gerrit.extensions.common.PluginDefinedInfo; import com.google.gerrit.index.query.QueryParseException; import com.google.gerrit.server.DynamicOptions.BeanProvider; import com.google.gerrit.server.change.ChangePluginDefinedInfoFactory; +import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.query.change.ChangeData; import com.google.inject.Inject; import com.googlesource.gerrit.plugins.task.TaskConfig.Task; @@ -29,33 +30,64 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.EnumSet; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.eclipse.jgit.errors.ConfigInvalidException; public class TaskAttributeFactory implements ChangePluginDefinedInfoFactory { - private static final FluentLogger log = FluentLogger.forEnclosingClass(); + public static final TaskPath MISSING_VIEW_PATH_CAPABILITY = + new TaskPath( + String.format( + "Can't perform operation, need %s capability", ViewPathsCapability.VIEW_PATHS)); + public enum Status { INVALID, UNKNOWN, + DUPLICATE, WAITING, READY, PASS, FAIL; } + public static class Statistics { + public long numberOfChanges; + public long numberOfChangeNodes; + public long numberOfDuplicates; + public long numberOfNodes; + public long numberOfTaskPluginAttributes; + public Object predicateCache; + public Object matchCache; + public Preloader.Statistics preloader; + public TaskTree.Statistics treeCaches; + } + public static class TaskAttribute { + public static class Statistics { + public boolean isApplicableRefreshRequired; + public boolean isSubNodeReloadRequired; + public boolean isTaskRefreshNeeded; + public Boolean hasUnfilterableSubNodes; + public Object nodesByBranchCache; + public Object properties; + } + public Boolean applicable; public Map<String, String> exported; public Boolean hasPass; public String hint; public Boolean inProgress; + public TaskPath path; public String name; + public Integer change; public Status status; public List<TaskAttribute> subTasks; public Long evaluationMilliSeconds; + public Statistics statistics; public TaskAttribute(String name) { this.name = name; @@ -64,29 +96,53 @@ public static class TaskPluginAttribute extends PluginDefinedInfo { public List<TaskAttribute> roots = new ArrayList<>(); + public Statistics queryStatistics; } + protected final String pluginName; protected final TaskTree definitions; protected final PredicateCache predicateCache; + protected final boolean hasViewPathsCapability; + protected final TaskPath.Factory taskPathFactory; + protected final TaskConfigCache taskConfigCache; protected Modules.MyOptions options; + protected TaskPluginAttribute lastTaskPluginAttribute; + protected Statistics statistics; @Inject - public TaskAttributeFactory(TaskTree definitions, PredicateCache predicateCache) { - this.definitions = definitions; + public TaskAttributeFactory( + String pluginName, + TaskTree.Factory taskTreeFactory, + PredicateCache predicateCache, + PermissionBackend permissionBackend, + TaskPath.Factory taskPathFactory, + TaskConfigCache taskConfigCache) { + this.pluginName = pluginName; + this.definitions = taskTreeFactory.create(taskConfigCache); this.predicateCache = predicateCache; + this.hasViewPathsCapability = + permissionBackend + .currentUser() + .testOrFalse(new PluginPermission(this.pluginName, ViewPathsCapability.VIEW_PATHS)); + this.taskPathFactory = taskPathFactory; + this.taskConfigCache = taskConfigCache; } @Override public Map<Change.Id, PluginDefinedInfo> createPluginDefinedInfos( - Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) { + Collection<ChangeData> cds, BeanProvider beanProvider, String plugin) { Map<Change.Id, PluginDefinedInfo> pluginInfosByChange = new HashMap<>(); options = (Modules.MyOptions) beanProvider.getDynamicBean(plugin); if (options.all || options.onlyApplicable || options.onlyInvalid) { + initStatistics(); for (PatchSetArgument psa : options.patchSetArguments) { - definitions.masquerade(psa); + taskConfigCache.masquerade(psa); } cds.forEach(cd -> pluginInfosByChange.put(cd.getId(), createWithExceptions(cd))); + if (lastTaskPluginAttribute != null) { + lastTaskPluginAttribute.queryStatistics = getStatistics(pluginInfosByChange); + } } return pluginInfosByChange; } @@ -94,38 +150,47 @@ protected PluginDefinedInfo createWithExceptions(ChangeData c) { TaskPluginAttribute a = new TaskPluginAttribute(); try { - for (Node node : definitions.getRootNodes(c)) { - if (node == null) { + for (Node root : definitions.getRootNodes(c)) { + if (root instanceof Node.Invalid) { a.roots.add(invalid()); } else { - new AttributeFactory(node).create().ifPresent(t -> a.roots.add(t)); + if (options.shouldFilterRoot(root.task.name())) { + continue; + } + new AttributeFactory(root).create().ifPresent(t -> a.roots.add(t)); } } - } catch (ConfigInvalidException | IOException e) { + } catch (ConfigInvalidException | IOException | StorageException e) { a.roots.add(invalid()); } if (a.roots.isEmpty()) { return null; } + lastTaskPluginAttribute = a; return a; } protected class AttributeFactory { public Node node; - public MatchCache matchCache; protected Task task; protected TaskAttribute attribute; protected AttributeFactory(Node node) { - this(node, new MatchCache(predicateCache, node.getChangeData())); - } - - protected AttributeFactory(Node node, MatchCache matchCache) { this.node = node; - this.matchCache = matchCache; this.task = node.task; - this.attribute = new TaskAttribute(task.name); + attribute = new TaskAttribute(task.name()); + if (options.includeStatistics) { + statistics.numberOfNodes++; + if (node.isChange()) { + statistics.numberOfChangeNodes++; + } + if (node.isDuplicate) { + statistics.numberOfDuplicates++; + } + attribute.statistics = new TaskAttribute.Statistics(); + attribute.statistics.properties = node.propertiesStatistics; + } } public Optional<TaskAttribute> create() { @@ -134,20 +199,37 @@ attribute.evaluationMilliSeconds = millis(); } - boolean applicable = matchCache.match(task.applicable); + boolean applicable; + try { + applicable = node.match(task.applicable); + } catch (QueryParseException e) { + return Optional.of(invalid()); + } if (!task.isVisible) { - if (!task.isTrusted || (!applicable && !options.onlyApplicable)) { + if (!node.isTrusted() || (!applicable && !options.onlyApplicable)) { return Optional.of(unknown()); } } if (applicable || !options.onlyApplicable) { - attribute.hasPass = task.pass != null || task.fail != null; - attribute.subTasks = getSubTasks(); + if (node.isChange()) { + attribute.change = node.getChangeData().getId().get(); + } + attribute.hasPass = !node.isDuplicate && (task.pass != null || task.fail != null); + if (!node.isDuplicate) { + attribute.subTasks = getSubTasks(); + } attribute.status = getStatus(); if (options.onlyInvalid && !isValidQueries()) { attribute.status = Status.INVALID; } + if (options.includePaths) { + if (hasViewPathsCapability) { + attribute.path = taskPathFactory.create(node.taskKey); + } else { + attribute.path = MISSING_VIEW_PATH_CAPABILITY; + } + } boolean groupApplicable = attribute.status != null; if (groupApplicable || !options.onlyApplicable) { @@ -157,26 +239,54 @@ if (!options.onlyApplicable) { attribute.applicable = applicable; } - if (task.inProgress != null) { - attribute.inProgress = matchCache.matchOrNull(task.inProgress); + if (!node.isDuplicate) { + if (task.inProgress != null) { + attribute.inProgress = node.matchOrNull(task.inProgress); + } + attribute.exported = task.exported.isEmpty() ? null : task.exported; } attribute.hint = getHint(attribute.status, task); - attribute.exported = task.exported.isEmpty() ? null : task.exported; if (options.evaluationTime) { attribute.evaluationMilliSeconds = millis() - attribute.evaluationMilliSeconds; } + addStatistics(attribute.statistics); return Optional.of(attribute); } } } - } catch (QueryParseException | RuntimeException e) { + } catch (IOException | RuntimeException | ConfigInvalidException e) { return Optional.of(invalid()); // bad applicability query } return Optional.empty(); } + protected TaskAttribute invalid() { + TaskAttribute invalid = TaskAttributeFactory.invalid(); + if (task.isVisible) { + invalid.name = task.name(); + } + return invalid; + } + + public void addStatistics(TaskAttribute.Statistics statistics) { + if (statistics != null) { + statistics.isApplicableRefreshRequired = node.properties.isApplicableRefreshRequired(); + statistics.isSubNodeReloadRequired = node.properties.isSubNodeReloadRequired(); + statistics.isTaskRefreshNeeded = node.properties.isTaskRefreshRequired(); + if (!statistics.isSubNodeReloadRequired) { + statistics.hasUnfilterableSubNodes = node.hasUnfilterableSubNodes; + } + if (node.nodesByBranch != null) { + statistics.nodesByBranchCache = node.nodesByBranch.getStatistics(); + } + } + } + protected Status getStatusWithExceptions() throws StorageException, QueryParseException { + if (node.isDuplicate) { + return Status.DUPLICATE; + } if (isAllNull(task.pass, task.fail, attribute.subTasks)) { // A leaf def has no defined subdefs. boolean hasDefinedSubtasks = @@ -198,7 +308,7 @@ } if (task.fail != null) { - if (matchCache.match(task.fail)) { + if (node.match(task.fail)) { // A FAIL definition is meant to be a hard blocking criteria // (like a CodeReview -2). Thus, if hard blocked, it is // irrelevant what the subtask states, or the PASS criteria are. @@ -212,7 +322,8 @@ } } - if (attribute.subTasks != null && !isAll(attribute.subTasks, Status.PASS)) { + if (attribute.subTasks != null + && !isAll(attribute.subTasks, EnumSet.of(Status.PASS, Status.DUPLICATE))) { // It is possible for a subtask's PASS criteria to change while // a parent task is executing, or even after the parent task // completes. This can result in the parent PASS criteria being @@ -224,7 +335,7 @@ return Status.WAITING; } - if (task.pass != null && !matchCache.match(task.pass)) { + if (task.pass != null && !node.match(task.pass)) { // Non-leaf tasks with no PASS criteria are supported in order // to support "grouping tasks" (tasks with no function aside from // organizing tasks). A task without a PASS criteria, cannot ever @@ -245,13 +356,15 @@ } } - protected List<TaskAttribute> getSubTasks() throws StorageException { + protected List<TaskAttribute> getSubTasks() + throws IOException, StorageException, ConfigInvalidException { List<TaskAttribute> subTasks = new ArrayList<>(); - for (Node subNode : node.getSubNodes()) { - if (subNode == null) { - subTasks.add(invalid()); + for (Node subNode : + options.onlyApplicable ? node.getApplicableSubNodes() : node.getSubNodes()) { + if (subNode instanceof Node.Invalid) { + subTasks.add(TaskAttributeFactory.invalid()); } else { - new AttributeFactory(subNode, matchCache).create().ifPresent(t -> subTasks.add(t)); + new AttributeFactory(subNode).create().ifPresent(t -> subTasks.add(t)); } } if (subTasks.isEmpty()) { @@ -262,9 +375,9 @@ protected boolean isValidQueries() { try { - matchCache.match(task.inProgress); - matchCache.match(task.fail); - matchCache.match(task.pass); + node.match(task.inProgress); + node.match(task.fail); + node.match(task.pass); return true; } catch (QueryParseException | RuntimeException e) { return false; @@ -276,7 +389,30 @@ return System.nanoTime() / 1000000; } - protected TaskAttribute invalid() { + public void initStatistics() { + if (options.includeStatistics) { + statistics = new Statistics(); + definitions.predicateCache.initStatistics(options.summaryCount); + definitions.matchCache.initStatistics(options.summaryCount); + definitions.preloader.initStatistics(options.summaryCount); + definitions.initStatistics(options.summaryCount); + } + } + + public Statistics getStatistics(Map<Change.Id, PluginDefinedInfo> pluginInfosByChange) { + if (statistics != null) { + statistics.numberOfChanges = pluginInfosByChange.size(); + statistics.numberOfTaskPluginAttributes = + pluginInfosByChange.values().stream().filter(tpa -> tpa != null).count(); + statistics.predicateCache = definitions.predicateCache.getStatistics(); + statistics.matchCache = definitions.matchCache.getStatistics(); + statistics.preloader = definitions.preloader.getStatistics(); + statistics.treeCaches = definitions.getStatistics(); + } + return statistics; + } + + protected static TaskAttribute invalid() { // For security reasons, do not expose the task name without knowing // the visibility which is derived from its applicability. TaskAttribute a = unknown(); @@ -284,17 +420,23 @@ return a; } - protected TaskAttribute unknown() { + protected static TaskAttribute unknown() { TaskAttribute a = new TaskAttribute("UNKNOWN"); a.status = Status.UNKNOWN; return a; } - protected String getHint(Status status, Task def) { - if (status == Status.READY) { - return def.readyHint; - } else if (status == Status.FAIL) { - return def.failHint; + protected static String getHint(Status status, Task def) { + if (status != null) { + switch (status) { + case READY: + return def.readyHint; + case FAIL: + return def.failHint; + case DUPLICATE: + return "Duplicate task is non blocking and empty to break the loop"; + default: + } } return null; } @@ -308,9 +450,9 @@ return true; } - protected static boolean isAll(Iterable<TaskAttribute> atts, Status state) { + protected static boolean isAll(Iterable<TaskAttribute> atts, Set<Status> states) { for (TaskAttribute att : atts) { - if (att.status != state) { + if (!states.contains(att.status)) { return false; } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java index 5e987ca..53897b5 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfig.java
@@ -14,21 +14,20 @@ package com.googlesource.gerrit.plugins.task; -import com.google.common.primitives.Primitives; import com.google.gerrit.common.Container; import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.server.git.meta.AbstractVersionedMetaData; -import java.lang.reflect.Field; +import com.googlesource.gerrit.plugins.task.util.Copier; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.eclipse.jgit.errors.ConfigInvalidException; +import java.util.stream.Collectors; /** Task Configuration file living in git */ public class TaskConfig extends AbstractVersionedMetaData { @@ -44,16 +43,19 @@ } } - protected class Section extends Container { + protected class SubSection extends Container { public TaskConfig config; + public final SubSectionKey subSection; - public Section() { + public SubSection(SubSectionKey s) { this.config = TaskConfig.this; + this.subSection = s; } } - public class TaskBase extends Section { + public class TaskBase extends SubSection { public String applicable; + public String duplicateKey; public Map<String, String> exported; public String fail; public String failHint; @@ -62,18 +64,20 @@ public String preloadTask; public Map<String, String> properties; public String readyHint; - public List<String> subTasks; - public List<String> subTasksExternals; - public List<String> subTasksFactories; - public List<String> subTasksFiles; + public List<ConfigSourcedValue> subTasks; + public List<ConfigSourcedValue> subTasksExternals; + public List<ConfigSourcedValue> subTasksFactories; + public List<ConfigSourcedValue> subTasksFiles; public boolean isVisible; - public boolean isTrusted; + public boolean isMasqueraded; - public TaskBase(SubSection s, boolean isVisible, boolean isTrusted) { + public TaskBase(SubSectionKey s, boolean isVisible, boolean isMasqueraded) { + super(s); this.isVisible = isVisible; - this.isTrusted = isTrusted; + this.isMasqueraded = isMasqueraded; applicable = getString(s, KEY_APPLICABLE, null); + duplicateKey = getString(s, KEY_DUPLICATE_KEY, null); exported = getProperties(s, KEY_EXPORT_PREFIX); fail = getString(s, KEY_FAIL, null); failHint = getString(s, KEY_FAIL_HINT, null); @@ -82,60 +86,50 @@ preloadTask = getString(s, KEY_PRELOAD_TASK, null); properties = getProperties(s, KEY_PROPERTIES_PREFIX); readyHint = getString(s, KEY_READY_HINT, null); - subTasks = getStringList(s, KEY_SUBTASK); - subTasksExternals = getStringList(s, KEY_SUBTASKS_EXTERNAL); - subTasksFactories = getStringList(s, KEY_SUBTASKS_FACTORY); - subTasksFiles = getStringList(s, KEY_SUBTASKS_FILE); + subTasks = + getStringList(s, KEY_SUBTASK).stream() + .map(subTask -> ConfigSourcedValue.create(s.file(), subTask)) + .collect(Collectors.toList()); + subTasksExternals = + getStringList(s, KEY_SUBTASKS_EXTERNAL).stream() + .map(subTask -> ConfigSourcedValue.create(s.file(), subTask)) + .collect(Collectors.toList()); + subTasksFactories = + getStringList(s, KEY_SUBTASKS_FACTORY).stream() + .map(subTask -> ConfigSourcedValue.create(s.file(), subTask)) + .collect(Collectors.toList()); + subTasksFiles = + getStringList(s, KEY_SUBTASKS_FILE).stream() + .map(fileName -> ConfigSourcedValue.create(s.file(), fileName)) + .collect(Collectors.toList()); } protected TaskBase(TaskBase base) { - copyDeclaredFields(TaskBase.class, base); + this(base.subSection); + Copier.shallowCopyDeclaredFields(TaskBase.class, base, this, false); } - protected <T> void copyDeclaredFields(Class<T> cls, T from) { - for (Field field : cls.getDeclaredFields()) { - try { - field.setAccessible(true); - Class<?> fieldCls = field.getType(); - Object val = field.get(from); - if (field.getType().isPrimitive() - || Primitives.isWrapperType(fieldCls) - || (val instanceof String) - || val == null) { - field.set(this, val); - } else if (val instanceof List) { - List<?> list = List.class.cast(val); - field.set(this, new ArrayList<>(list)); - } else if (val instanceof Map) { - Map<?, ?> map = Map.class.cast(val); - field.set(this, new HashMap<>(map)); - } else if (field.getName().equals("this$0")) { // Don't copy internal final field - } else { - throw new RuntimeException( - "Don't know how to deep copy " + fieldValueToString(field, val)); - } - } catch (IllegalAccessException e) { - throw new RuntimeException( - "Cannot access field to copy it " + fieldValueToString(field, "unknown")); - } - } - } - - protected String fieldValueToString(Field field, Object val) { - return "field:" + field.getName() + " value:" + val + " type:" + field.getType(); + protected TaskBase(SubSectionKey s) { + super(s); } } - public class Task extends TaskBase { - public String name; + public class Task extends TaskBase implements Cloneable { + public final TaskKey key; - public Task(SubSection s, boolean isVisible, boolean isTrusted) { - super(s, isVisible, isTrusted); - name = s.subSection; + public Task(SubSectionKey s, boolean isVisible, boolean isMasqueraded) { + super(s, isVisible, isMasqueraded); + key = TaskKey.create(s); } - protected Task(TaskBase base) { - super(base); + public Task(TasksFactory tasks, String name) { + super(tasks); + key = TaskKey.create(tasks.subSection, name); + } + + public Task(SubSectionKey s) { + super(s); + key = TaskKey.create(s); } protected Map<String, String> getAllProperties() { @@ -144,104 +138,103 @@ return all; } - protected void setExpandedProperties(Map<String, String> expanded) { - properties.clear(); - properties.putAll(expanded); - for (String property : exported.keySet()) { - exported.put(property, properties.get(property)); - } + public String name() { + return key.task(); + } + + public FileKey file() { + return key.subSection().file(); + } + + public TaskKey key() { + return key; } } public class TasksFactory extends TaskBase { public String namesFactory; - public TasksFactory(SubSection s, boolean isVisible, boolean isTrusted) { - super(s, isVisible, isTrusted); + public TasksFactory(SubSectionKey s, boolean isVisible, boolean isMasqueraded) { + super(s, isVisible, isMasqueraded); namesFactory = getString(s, KEY_NAMES_FACTORY, null); } } - public class NamesFactory extends Section { + public class NamesFactory extends SubSection implements Cloneable { public String changes; public List<String> names; public String type; - public NamesFactory(SubSection s) { + public NamesFactory(SubSectionKey s) { + super(s); changes = getString(s, KEY_CHANGES, null); names = getStringList(s, KEY_NAME); type = getString(s, KEY_TYPE, null); } } - public class External extends Section { + public class External extends SubSection { public String name; public String file; public String user; - public External(SubSection s) { - name = s.subSection; + public External(SubSectionKey s) { + super(s); + name = s.subSection(); file = getString(s, KEY_FILE, null); user = getString(s, KEY_USER, null); } } - protected static final Pattern OPTIONAL_TASK_PATTERN = - Pattern.compile("([^ |]*( *[^ |])*) *\\| *"); + public static final String SEP = "\0"; - protected static final String SECTION_EXTERNAL = "external"; - protected static final String SECTION_NAMES_FACTORY = "names-factory"; - protected static final String SECTION_ROOT = "root"; - protected static final String SECTION_TASK = "task"; - protected static final String SECTION_TASKS_FACTORY = "tasks-factory"; - protected static final String KEY_APPLICABLE = "applicable"; - protected static final String KEY_CHANGES = "changes"; - protected static final String KEY_EXPORT_PREFIX = "export-"; - protected static final String KEY_FAIL = "fail"; - protected static final String KEY_FAIL_HINT = "fail-hint"; - protected static final String KEY_FILE = "file"; - protected static final String KEY_IN_PROGRESS = "in-progress"; - protected static final String KEY_NAME = "name"; - protected static final String KEY_NAMES_FACTORY = "names-factory"; - protected static final String KEY_PASS = "pass"; - protected static final String KEY_PRELOAD_TASK = "preload-task"; - protected static final String KEY_PROPERTIES_PREFIX = "set-"; - protected static final String KEY_READY_HINT = "ready-hint"; - protected static final String KEY_SUBTASK = "subtask"; - protected static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external"; - protected static final String KEY_SUBTASKS_FACTORY = "subtasks-factory"; - protected static final String KEY_SUBTASKS_FILE = "subtasks-file"; - protected static final String KEY_TYPE = "type"; - protected static final String KEY_USER = "user"; + public static final String SECTION_EXTERNAL = "external"; + public static final String SECTION_NAMES_FACTORY = "names-factory"; + public static final String SECTION_ROOT = "root"; + public static final String SECTION_TASK = TaskKey.CONFIG_SECTION; + public static final String SECTION_TASKS_FACTORY = TaskKey.CONFIG_TASKS_FACTORY; + public static final String KEY_APPLICABLE = "applicable"; + public static final String KEY_CHANGES = "changes"; + public static final String KEY_DUPLICATE_KEY = "duplicate-key"; + public static final String KEY_EXPORT_PREFIX = "export-"; + public static final String KEY_FAIL = "fail"; + public static final String KEY_FAIL_HINT = "fail-hint"; + public static final String KEY_FILE = "file"; + public static final String KEY_IN_PROGRESS = "in-progress"; + public static final String KEY_NAME = "name"; + public static final String KEY_NAMES_FACTORY = "names-factory"; + public static final String KEY_PASS = "pass"; + public static final String KEY_PRELOAD_TASK = "preload-task"; + public static final String KEY_PROPERTIES_PREFIX = "set-"; + public static final String KEY_READY_HINT = "ready-hint"; + public static final String KEY_SUBTASK = "subtask"; + public static final String KEY_SUBTASKS_EXTERNAL = "subtasks-external"; + public static final String KEY_SUBTASKS_FACTORY = "subtasks-factory"; + public static final String KEY_SUBTASKS_FILE = "subtasks-file"; + public static final String KEY_TYPE = "type"; + public static final String KEY_USER = "user"; + protected final FileKey file; public boolean isVisible; - public boolean isTrusted; + public boolean isMasqueraded; - public Task createTask(TasksFactory tasks, String name) { - Task task = new Task(tasks); - task.name = name; - return task; + public TaskConfig(FileKey file, boolean isVisible, boolean isMasqueraded) { + this(file.branch(), file, isVisible, isMasqueraded); } - public TaskConfig(BranchNameKey branch, String fileName, boolean isVisible, boolean isTrusted) { - super(branch, fileName); + public TaskConfig( + BranchNameKey masqueraded, FileKey file, boolean isVisible, boolean isMasqueraded) { + super(masqueraded, file.file()); + this.file = file; this.isVisible = isVisible; - this.isTrusted = isTrusted; - } - - public List<Task> getRootTasks() { - return getTasks(SECTION_ROOT); - } - - public List<Task> getTasks() { - return getTasks(SECTION_TASK); + this.isMasqueraded = isMasqueraded; } protected List<Task> getTasks(String type) { List<Task> tasks = new ArrayList<>(); // No need to get a task with no name (what would we call it?) for (String task : cfg.getSubsections(type)) { - tasks.add(new Task(new SubSection(type, task), isVisible, isTrusted)); + tasks.add(new Task(subSectionKey(type, task), isVisible, isMasqueraded)); } return tasks; } @@ -255,103 +248,77 @@ return externals; } - /* returs null only if optional and not found */ - public Task getTaskOptional(String name) throws ConfigInvalidException { - int end = 0; - Matcher m = OPTIONAL_TASK_PATTERN.matcher(name); - while (m.find()) { - end = m.end(); - Task task = getTaskOrNull(m.group(1)); - if (task != null) { - return task; - } - } - - String last = name.substring(end); - if (!"".equals(last)) { // Last entry was not optional - Task task = getTaskOrNull(last); - if (task != null) { - return task; - } - throw new ConfigInvalidException("task not defined"); - } - return null; - } - - /* returns null if not found */ - protected Task getTaskOrNull(String name) { - SubSection subSection = new SubSection(SECTION_TASK, name); - return getNames(subSection).isEmpty() ? null : new Task(subSection, isVisible, isTrusted); + protected Optional<Task> getOptionalTask(String name) { + SubSectionKey subSection = subSectionKey(SECTION_TASK, name); + return getNames(subSection).isEmpty() + ? Optional.empty() + : Optional.of(new Task(subSection, isVisible, isMasqueraded)); } public TasksFactory getTasksFactory(String name) { - return new TasksFactory(new SubSection(SECTION_TASKS_FACTORY, name), isVisible, isTrusted); + return new TasksFactory(subSectionKey(SECTION_TASKS_FACTORY, name), isVisible, isMasqueraded); } public NamesFactory getNamesFactory(String name) { - return new NamesFactory(new SubSection(SECTION_NAMES_FACTORY, name)); + return new NamesFactory(subSectionKey(SECTION_NAMES_FACTORY, name)); } public External getExternal(String name) { - return getExternal(new SubSection(SECTION_EXTERNAL, name)); + return getExternal(subSectionKey(SECTION_EXTERNAL, name)); } - protected External getExternal(SubSection s) { + protected External getExternal(SubSectionKey s) { return new External(s); } - protected Map<String, String> getProperties(SubSection s, String prefix) { + protected Map<String, String> getProperties(SubSectionKey s, String prefix) { Map<String, String> valueByName = new HashMap<>(); for (Map.Entry<String, String> e : getStringByName(s, getMatchingNames(s, prefix + ".+")).entrySet()) { String name = e.getKey(); valueByName.put(name.substring(prefix.length()), e.getValue()); } - return valueByName; + return Collections.unmodifiableMap(valueByName); } - protected Map<String, String> getStringByName(SubSection s, Iterable<String> names) { + protected Map<String, String> getStringByName(SubSectionKey s, Iterable<String> names) { Map<String, String> valueByName = new HashMap<>(); for (String name : names) { valueByName.put(name, getString(s, name)); } - return valueByName; + return Collections.unmodifiableMap(valueByName); } - protected Set<String> getMatchingNames(SubSection s, String match) { + protected Set<String> getMatchingNames(SubSectionKey s, String match) { Set<String> matched = new HashSet<>(); for (String name : getNames(s)) { if (name.matches(match)) { matched.add(name); } } - return matched; + return Collections.unmodifiableSet(matched); } - protected Set<String> getNames(SubSection s) { - return cfg.getNames(s.section, s.subSection); + protected Set<String> getNames(SubSectionKey s) { + return cfg.getNames(s.section(), s.subSection()); } - protected String getString(SubSection s, String key, String def) { + protected String getString(SubSectionKey s, String key, String def) { String v = getString(s, key); return v != null ? v : def; } - protected String getString(SubSection s, String key) { - return cfg.getString(s.section, s.subSection, key); + protected String getString(SubSectionKey s, String key) { + return cfg.getString(s.section(), s.subSection(), key); } - protected List<String> getStringList(SubSection s, String key) { - return Arrays.asList(cfg.getStringList(s.section, s.subSection, key)); + protected List<String> getStringList(SubSectionKey s, String key) { + List<String> stringList = Arrays.asList(cfg.getStringList(s.section(), s.subSection(), key)); + stringList.replaceAll(str -> str == null ? "" : str); + return Collections.unmodifiableList(stringList); } - protected static class SubSection { - public final String section; - public final String subSection; - - protected SubSection(String section, String subSection) { - this.section = section; - this.subSection = subSection; - } + protected SubSectionKey subSectionKey(String section, String subSection) { + return SubSectionKey.create(file, section, subSection); } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java similarity index 70% rename from src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java rename to src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java index f790ccc..648c729 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigFactory.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskConfigCache.java
@@ -17,9 +17,11 @@ import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.BranchNameKey; import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllProjectsNameProvider; import com.google.gerrit.server.git.GitRepositoryManager; import com.google.gerrit.server.permissions.PermissionBackend; import com.google.gerrit.server.permissions.PermissionBackendException; @@ -32,12 +34,9 @@ import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.lib.Repository; -public class TaskConfigFactory { +public class TaskConfigCache { private static final FluentLogger log = FluentLogger.forEnclosingClass(); - protected static final String EXTENSION = ".config"; - protected static final String DEFAULT = "task" + EXTENSION; - protected final GitRepositoryManager gitMgr; protected final PermissionBackend permissionBackend; @@ -45,21 +44,22 @@ protected final AllProjectsName allProjects; protected final Map<BranchNameKey, PatchSetArgument> psaMasquerades = new HashMap<>(); + protected final Map<FileKey, TaskConfig> taskCfgByFile = new HashMap<>(); @Inject - protected TaskConfigFactory( - AllProjectsName allProjects, + protected TaskConfigCache( + AllProjectsNameProvider allProjectsNameProvider, GitRepositoryManager gitMgr, PermissionBackend permissionBackend, CurrentUser user) { - this.allProjects = allProjects; + this.allProjects = allProjectsNameProvider.get(); this.gitMgr = gitMgr; this.permissionBackend = permissionBackend; this.user = user; } public TaskConfig getRootConfig() throws ConfigInvalidException, IOException { - return getTaskConfig(getRootBranch(), DEFAULT, true); + return getTaskConfig(FileKey.create(getRootBranch(), TaskFileConstants.TASK_CFG)); } public void masquerade(PatchSetArgument psa) { @@ -67,26 +67,39 @@ } protected BranchNameKey getRootBranch() { - return BranchNameKey.create(allProjects, "refs/meta/config"); + return BranchNameKey.create(allProjects, RefNames.REFS_CONFIG); } - public TaskConfig getTaskConfig(BranchNameKey branch, String fileName, boolean isTrusted) - throws ConfigInvalidException, IOException { + public TaskConfig getTaskConfig(FileKey key) throws ConfigInvalidException, IOException { + TaskConfig cfg = taskCfgByFile.get(key); + if (cfg == null) { + cfg = loadTaskConfig(key); + taskCfgByFile.put(key, cfg); + } + return cfg; + } + + private TaskConfig loadTaskConfig(FileKey file) throws ConfigInvalidException, IOException { + BranchNameKey branch = file.branch(); PatchSetArgument psa = psaMasquerades.get(branch); boolean visible = true; // invisible psas are filtered out by commandline + boolean isMasqueraded = false; if (psa == null) { - visible = canRead(branch); + visible = isVisible(branch); } else { - isTrusted = false; + isMasqueraded = true; branch = BranchNameKey.create(psa.change.getProject(), psa.patchSet.refName()); } - Project.NameKey project = branch.project(); - TaskConfig cfg = new TaskConfig(branch, fileName, visible, isTrusted); + Project.NameKey project = file.branch().project(); + TaskConfig cfg = + isMasqueraded + ? new TaskConfig(branch, file, visible, isMasqueraded) + : new TaskConfig(file, visible, isMasqueraded); try (Repository git = gitMgr.openRepository(project)) { cfg.load(project, git); } catch (IOException e) { - log.atWarning().withCause(e).log("Failed to load %s for %s", fileName, project); + log.atWarning().withCause(e).log("Failed to load %s for %s", file.file(), project); throw e; } catch (ConfigInvalidException e) { throw e; @@ -94,7 +107,7 @@ return cfg; } - public boolean canRead(BranchNameKey branch) { + public boolean isVisible(BranchNameKey branch) { try { PermissionBackend.ForProject permissions = permissionBackend.user(user).project(branch.project());
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java new file mode 100644 index 0000000..5b9bef0 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpression.java
@@ -0,0 +1,106 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.eclipse.jgit.errors.ConfigInvalidException; + +/** + * A TaskExpression represents a config string pointing to an expression which includes zero or more + * task references separated by a '|', and potentially termintated by a '|'. If the expression is + * not terminated by a '|' it indicates that task resolution of at least one task is required. Task + * selection priority is from left to right. This can be expressed as: + * + * <pre> + * TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ] + * </pre> + * + * <a href="file:../../../../../../antlr4/com/googlesource/gerrit/plugins/task/TaskReference.g4">See + * this for Task Reference</a> + * + * <p>Example expressions to prioritized names and requirements: + * + * <ul> + * <li> + * <pre> "simple" -> ("simple") required</pre> + * <li> + * <pre> "world | peace" -> ("world", "peace") required</pre> + * <li> + * <pre> "shadenfreud |" -> ("shadenfreud") optional</pre> + * <li> + * <pre> "foo | bar |" -> ("foo", "bar") optional</pre> + * </ul> + */ +public class TaskExpression implements Iterable<TaskKey> { + public interface Factory { + TaskExpression create(FileKey key, String expression); + } + + protected static final Pattern EXPRESSION_PATTERN = Pattern.compile("([^ |]+[^|]*)(\\|)?"); + protected final TaskExpressionKey key; + protected final TaskReference.Factory taskReferenceFactory; + + @Inject + public TaskExpression( + TaskReference.Factory taskReferenceFactory, + @Assisted FileKey key, + @Assisted String expression) { + this.key = TaskExpressionKey.create(key, expression); + this.taskReferenceFactory = taskReferenceFactory; + } + + @Override + public Iterator<TaskKey> iterator() { + return new Iterator<TaskKey>() { + Matcher m = EXPRESSION_PATTERN.matcher(key.expression()); + Boolean hasNext; + boolean optional; + + @Override + public boolean hasNext() { + if (hasNext == null) { + hasNext = m.find(); + if (hasNext) { + optional = m.group(2) != null; + } + } + if (!hasNext && !optional) { + return true; // fake it so next() throws an Exception + } + return hasNext; + } + + @Override + public TaskKey next() { + // Can't use @SuppressWarnings("ReturnValueIgnored") on method call + boolean ignored = hasNext(); // in case next() was (re)called w/o calling hasNext() + if (!hasNext) { + throw new NoSuchElementException("No more names, yet expression was not optional"); + } + hasNext = null; + try { + return taskReferenceFactory.create(key.file(), m.group(1)).getTaskKey(); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + }; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java new file mode 100644 index 0000000..6fcd30d --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskExpressionKey.java
@@ -0,0 +1,34 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.auto.value.AutoValue; +import com.google.gerrit.entities.BranchNameKey; + +/** A key for TaskExpression. */ +@AutoValue +public abstract class TaskExpressionKey { + public static TaskExpressionKey create(FileKey file, String expression) { + return new AutoValue_TaskExpressionKey(file, expression); + } + + public BranchNameKey branch() { + return file().branch(); + } + + public abstract FileKey file(); + + public abstract String expression(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java new file mode 100644 index 0000000..904c933 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskFileConstants.java
@@ -0,0 +1,20 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +public final class TaskFileConstants { + public static final String TASK_DIR = "task"; + public static final String TASK_CFG = "task.config"; +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java new file mode 100644 index 0000000..bd0b683 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskKey.java
@@ -0,0 +1,204 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.auto.value.AutoValue; +import com.google.common.base.Preconditions; +import com.google.gerrit.entities.AccountGroup; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.eclipse.jgit.errors.ConfigInvalidException; + +/** An immutable reference to a task in task config file. */ +@AutoValue +public abstract class TaskKey { + protected static final String CONFIG_SECTION = "task"; + protected static final String CONFIG_TASKS_FACTORY = "tasks-factory"; + + /** Creates a TaskKey with task name as the name of sub section. */ + public static TaskKey create(SubSectionKey section) { + return create(section, section.subSection()); + } + + /** Creates a TaskKey with given FileKey and task name and sub section's name as 'task'. */ + public static TaskKey create(FileKey file, String task) { + return create(SubSectionKey.create(file, CONFIG_SECTION, task)); + } + + /** Creates a TaskKey from a sub section and task name, generally used by TasksFactory. */ + public static TaskKey create(SubSectionKey section, String task) { + return new AutoValue_TaskKey(section, task); + } + + public BranchNameKey branch() { + return subSection().file().branch(); + } + + public abstract SubSectionKey subSection(); + + public abstract String task(); + + public boolean isTasksFactoryGenerated() { + return subSection().section().equals(CONFIG_TASKS_FACTORY); + } + + public static class Builder { + protected final AccountCache accountCache; + protected final AllProjectsName allProjectsName; + protected final AllUsersName allUsersName; + protected final FileKey relativeTo; + protected BranchNameKey branch; + protected String file; + protected String task; + protected GroupCache groupCache; + + Builder( + FileKey relativeTo, + AllProjectsName allProjectsName, + AllUsersName allUsersName, + AccountCache accountCache, + GroupCache groupCache) { + this.relativeTo = relativeTo; + this.allProjectsName = allProjectsName; + this.allUsersName = allUsersName; + this.accountCache = accountCache; + this.groupCache = groupCache; + } + + public TaskKey buildTaskKey() { + return isReferencingAnotherRef() ? getAnotherRefTask() : getSameRefTask(); + } + + protected TaskKey getAnotherRefTask() { + return TaskKey.create( + isReferencingRootFile() + ? FileKey.create(branch, TaskFileConstants.TASK_CFG) + : FileKey.create(branch, file), + task); + } + + protected TaskKey getSameRefTask() { + return TaskKey.create( + isRelativePath() ? relativeTo : FileKey.create(relativeTo.branch(), file), task); + } + + public void setAbsolute() { + file = TaskFileConstants.TASK_DIR; + } + + public void setPath(Path path) throws ConfigInvalidException { + Path parentDir = Paths.get(relativeTo.file()).getParent(); + if (parentDir == null) { + parentDir = Paths.get(TaskFileConstants.TASK_DIR); + } + + file = + isRelativePath() + ? parentDir.resolve(path).toString() + : Paths.get(file).resolve(path).toString(); + throwIfInvalidPath(); + } + + public void setRefRootFile() throws ConfigInvalidException { + Preconditions.checkState(!isFileAlreadySet()); + file = TaskFileConstants.TASK_CFG; + } + + public void setTaskName(String task) { + this.task = task; + } + + public void setUsername(String username) throws ConfigInvalidException { + branch = + BranchNameKey.create( + allUsersName, + RefNames.refsUsers( + accountCache + .getByUsername(username) + .orElseThrow( + () -> new ConfigInvalidException("Cannot resolve username: " + username)) + .account() + .id())); + } + + public void setGroupName(String groupName) throws ConfigInvalidException { + branch = + BranchNameKey.create( + allUsersName, + RefNames.refsGroups( + groupCache + .get(AccountGroup.nameKey(groupName)) + .orElseThrow( + () -> + new ConfigInvalidException( + String.format("Cannot resolve group name: %s", groupName))) + .getGroupUUID())); + } + + public void setGroupUUID(String uuid) throws ConfigInvalidException { + branch = + BranchNameKey.create( + allUsersName, + RefNames.refsGroups( + groupCache + .get(AccountGroup.uuid(uuid)) + .orElseThrow( + () -> + new ConfigInvalidException( + String.format("Cannot resolve group uuid: %s", uuid))) + .getGroupUUID())); + } + + public void setReferringAllProjectsTask() { + branch = BranchNameKey.create(allProjectsName, RefNames.REFS_CONFIG); + } + + protected void throwIfInvalidPath() throws ConfigInvalidException { + Path path = Paths.get(file); + if (!path.startsWith(TaskFileConstants.TASK_DIR) + && !path.equals(Paths.get(TaskFileConstants.TASK_CFG))) { + throw new ConfigInvalidException( + "Invalid config location, path should be " + + TaskFileConstants.TASK_CFG + + " or under " + + TaskFileConstants.TASK_DIR + + " directory"); + } + } + + /** Returns true when the path implies relative or same file. */ + protected boolean isRelativePath() { + return file == null; + } + + protected boolean isFileAlreadySet() { + return file != null; + } + + protected boolean isReferencingRootFile() { + return file == null; + } + + protected boolean isReferencingAnotherRef() { + return branch != null; + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java new file mode 100644 index 0000000..c0c5d8c --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskPath.java
@@ -0,0 +1,76 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.Project; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.Accounts; +import com.google.gerrit.server.config.AllUsersNameProvider; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.io.IOException; +import java.util.Optional; +import org.eclipse.jgit.errors.ConfigInvalidException; + +public class TaskPath { + public interface Factory { + TaskPath create(TaskKey key); + } + + protected String name; + protected String type; + protected String tasksFactory; + protected String user; + protected String project; + protected String ref; + protected String file; + protected String error; + + @Inject + public TaskPath(AllUsersNameProvider allUsers, Accounts accounts, @Assisted TaskKey key) { + name = key.task(); + type = key.subSection().section(); + tasksFactory = key.isTasksFactoryGenerated() ? key.subSection().subSection() : null; + user = getUserOrNull(accounts, allUsers.get(), key); + project = key.branch().project().get(); + ref = key.branch().branch(); + file = key.subSection().file().file(); + } + + public TaskPath(String error) { + this.error = error; + } + + private String getUserOrNull(Accounts accounts, Project.NameKey allUsers, TaskKey key) { + try { + if (allUsers.get().equals(key.branch().project().get())) { + String ref = key.branch().branch(); + Account.Id id = Account.Id.fromRef(ref); + if (id != null) { + Optional<AccountState> state = accounts.get(id); + if (state.isPresent()) { + Optional<String> userName = state.get().userName(); + if (userName.isPresent()) { + return userName.get(); + } + } + } + } + } catch (ConfigInvalidException | IOException e) { + } + return null; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java new file mode 100644 index 0000000..a7824ac --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskReference.java
@@ -0,0 +1,180 @@ +// Copyright (C) 2020 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.common.annotations.VisibleForTesting; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AllProjectsNameProvider; +import com.google.gerrit.server.config.AllUsersNameProvider; +import com.google.inject.Inject; +import com.google.inject.assistedinject.Assisted; +import java.nio.file.Paths; +import java.util.NoSuchElementException; +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.Lexer; +import org.antlr.v4.runtime.RecognitionException; +import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.eclipse.jgit.errors.ConfigInvalidException; + +/** This class is used by TaskExpression to decode the task from task reference. */ +public class TaskReference { + protected final String reference; + protected final TaskKey.Builder taskKeyBuilder; + + interface Factory { + TaskReference create(FileKey relativeTo, String reference); + } + + @Inject + public TaskReference( + AllProjectsNameProvider allProjectsNameProvider, + AllUsersNameProvider allUsersNameProvider, + AccountCache accountCache, + GroupCache groupCache, + @Assisted FileKey relativeTo, + @Assisted String reference) { + this( + new TaskKey.Builder( + relativeTo, + allProjectsNameProvider.get(), + allUsersNameProvider.get(), + accountCache, + groupCache), + reference); + } + + @VisibleForTesting + public TaskReference(TaskKey.Builder taskKeyBuilder, String reference) { + this.taskKeyBuilder = taskKeyBuilder; + this.reference = reference.trim(); + if (reference.isEmpty()) { + throw new NoSuchElementException(); + } + } + + public TaskKey getTaskKey() throws ConfigInvalidException { + ParseTreeWalker walker = new ParseTreeWalker(); + try { + walker.walk(new TaskReferenceListener(taskKeyBuilder), parse()); + } catch (RuntimeConfigInvalidException e) { + throw e.checkedException; + } + return taskKeyBuilder.buildTaskKey(); + } + + protected ParseTree parse() { + Lexer lexer = new TaskReferenceLexer(CharStreams.fromString(reference)); + lexer.removeErrorListeners(); + lexer.addErrorListener(TaskReferenceErrorListener.INSTANCE); + return new TaskReferenceParser(new CommonTokenStream(lexer)).reference(); + } + + protected static class TaskReferenceErrorListener extends BaseErrorListener { + protected static final TaskReferenceErrorListener INSTANCE = new TaskReferenceErrorListener(); + + @Override + public void syntaxError( + Recognizer<?, ?> recognizer, + Object offendingSymbol, + int line, + int charPositionInLine, + String msg, + RecognitionException e) { + throw new NoSuchElementException(); + } + } + + protected class TaskReferenceListener extends TaskReferenceBaseListener { + TaskKey.Builder builder; + + TaskReferenceListener(TaskKey.Builder builder) { + this.builder = builder; + } + + @Override + public void enterAbsolute(TaskReferenceParser.AbsoluteContext ctx) { + builder.setAbsolute(); + } + + @Override + public void enterRelative(TaskReferenceParser.RelativeContext ctx) { + try { + builder.setPath( + ctx.dir().stream() + .map(dir -> Paths.get(dir.NAME().getText())) + .reduce(Paths.get(""), (a, b) -> a.resolve(b)) + .resolve(ctx.NAME().getText())); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + + @Override + public void enterReference(TaskReferenceParser.ReferenceContext ctx) { + builder.setTaskName(ctx.TASK().getText()); + } + + @Override + public void enterFile_path(TaskReferenceParser.File_pathContext ctx) { + if (ctx.ALL_PROJECTS_ROOT() != null || (ctx.FWD_SLASH() != null && ctx.absolute() != null)) { + builder.setReferringAllProjectsTask(); + } + + if (ctx.absolute() == null && ctx.relative() == null) { + try { + builder.setRefRootFile(); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + } + + @Override + public void enterUser(TaskReferenceParser.UserContext ctx) { + try { + builder.setUsername(ctx.NAME().getText()); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + + @Override + public void enterGroup_name(TaskReferenceParser.Group_nameContext ctx) { + try { + String groupName = + ctx.NAME() == null + ? (ctx.NAME_WITH_SPACES() == null ? "" : ctx.NAME_WITH_SPACES().getText()) + : ctx.NAME().getText(); + builder.setGroupName(groupName); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + + @Override + public void enterGroup_uuid(TaskReferenceParser.Group_uuidContext ctx) { + try { + builder.setGroupUUID(ctx.INTERNAL_GROUP_UUID().getText()); + } catch (ConfigInvalidException e) { + throw new RuntimeConfigInvalidException(e); + } + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java index 7079918..5586774 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/TaskTree.java
@@ -14,6 +14,8 @@ package com.googlesource.gerrit.plugins.task; +import static java.util.stream.Collectors.toList; + import com.google.common.flogger.FluentLogger; import com.google.gerrit.entities.Account; import com.google.gerrit.entities.BranchNameKey; @@ -30,22 +32,33 @@ import com.google.gerrit.server.query.change.ChangeQueryProcessor; import com.google.inject.Inject; import com.google.inject.Provider; +import com.google.inject.assistedinject.Assisted; import com.googlesource.gerrit.plugins.task.TaskConfig.External; import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory; import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactoryType; import com.googlesource.gerrit.plugins.task.TaskConfig.Task; import com.googlesource.gerrit.plugins.task.TaskConfig.TasksFactory; -import com.googlesource.gerrit.plugins.task.cli.PatchSetArgument; +import com.googlesource.gerrit.plugins.task.properties.Properties; +import com.googlesource.gerrit.plugins.task.statistics.HitHashMap; +import com.googlesource.gerrit.plugins.task.statistics.HitHashMapOfCollection; +import com.googlesource.gerrit.plugins.task.statistics.StatisticsMap; +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; +import com.googlesource.gerrit.plugins.task.statistics.TracksStatistics; import java.io.IOException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.util.StringUtils; /** * Add structure to access the task definitions from the config as a tree. @@ -55,17 +68,46 @@ */ public class TaskTree { private static final FluentLogger log = FluentLogger.forEnclosingClass(); + + public interface Factory { + TaskTree create(@Assisted TaskConfigCache taskConfigCache); + } + + @FunctionalInterface + public interface NodeFactory { + Node create(NodeList parent, Task definition) throws Exception; + } + + public static class Statistics { + public Object definitionsPerSubSectionCache; + public Object definitionsByBranchBySubSectionCache; + public Object changesByNamesFactoryQueryCache; + public Properties.Statistics properties; + public transient int summaryCount; + } + protected static final String TASK_DIR = "task"; protected final AccountResolver accountResolver; protected final AllUsersNameProvider allUsers; protected final CurrentUser user; - protected final TaskConfigFactory taskFactory; - protected final Root root = new Root(); + protected final PredicateCache predicateCache; + protected final MatchCache matchCache; + protected final Preloader preloader; + protected final TaskConfigCache taskConfigCache; + protected final TaskExpression.Factory taskExpressionFactory; + protected final NodeList root = new NodeList(); protected final Provider<ChangeQueryBuilder> changeQueryBuilderProvider; protected final Provider<ChangeQueryProcessor> changeQueryProcessorProvider; + protected final StatisticsMap<String, List<ChangeData>> changesByNamesFactoryQuery = + new HitHashMap<>(); + protected final StatisticsMap<SubSectionKey, List<Task>> definitionsBySubSection = + new HitHashMapOfCollection<>(); + protected final StatisticsMap<SubSectionKey, Map<BranchNameKey, List<Task>>> + definitionsByBranchBySubSection = new HitHashMap<>(); protected ChangeData changeData; + protected Statistics statistics; @Inject public TaskTree( @@ -73,266 +115,562 @@ AllUsersNameProvider allUsers, AnonymousUser anonymousUser, CurrentUser user, - TaskConfigFactory taskFactory, Provider<ChangeQueryBuilder> changeQueryBuilderProvider, - Provider<ChangeQueryProcessor> changeQueryProcessorProvider) { + Provider<ChangeQueryProcessor> changeQueryProcessorProvider, + PredicateCache predicateCache, + TaskExpression.Factory taskExpressionFactory, + Preloader.Factory preloaderFactory, + @Assisted TaskConfigCache taskConfigCache) { this.accountResolver = accountResolver; this.allUsers = allUsers; this.user = user != null ? user : anonymousUser; - this.taskFactory = taskFactory; this.changeQueryProcessorProvider = changeQueryProcessorProvider; this.changeQueryBuilderProvider = changeQueryBuilderProvider; + this.predicateCache = predicateCache; + this.matchCache = new MatchCache(predicateCache); + this.taskConfigCache = taskConfigCache; + this.taskExpressionFactory = taskExpressionFactory; + this.preloader = preloaderFactory.create(taskConfigCache); } - public void masquerade(PatchSetArgument psa) { - taskFactory.masquerade(psa); - } - - public List<Node> getRootNodes(ChangeData changeData) throws ConfigInvalidException, IOException { + public List<Node> getRootNodes(ChangeData changeData) + throws ConfigInvalidException, IOException, StorageException { this.changeData = changeData; - return root.getRootNodes(); - } - - public Node createNodeOrNull(NodeList parent, Task definition) { - try { - return new Node(parent, definition); - } catch (Exception e) { - return null; - } + root.path = Collections.emptyList(); + root.duplicateKeys = Collections.emptyList(); + return root.getSubNodes(); } protected class NodeList { protected NodeList parent = null; - protected LinkedList<String> path = new LinkedList<>(); - protected List<Node> nodes; - protected Set<String> names = new HashSet<>(); + protected Collection<String> path; + protected Collection<String> duplicateKeys; + protected Map<TaskKey, Node> cachedNodeByTask = new HashMap<>(); + protected List<Node> cachedNodes; - protected void addSubDefinitions(List<Task> defs) { - for (Task def : defs) { - addSubDefinition(def); + protected List<Node> getSubNodes() + throws ConfigInvalidException, IOException, StorageException { + if (cachedNodes != null) { + return refresh(cachedNodes); } + return cachedNodes = loadSubNodes(); } - protected void addSubDefinition(Task def) { - addSubDefinition(def, (d, c) -> createNodeOrNull(d, c)); - } - - protected void addSubDefinition(Task def, BiFunction<NodeList, Task, Node> nodeConstructor) { - Node node = null; - if (def != null && !path.contains(def.name) && names.add(def.name)) { - // path check above detects looping definitions - // names check above detects duplicate subtasks - node = nodeConstructor.apply(this, def); - } - nodes.add(node); + protected List<Node> loadSubNodes() + throws ConfigInvalidException, IOException, StorageException { + return new SubNodeFactory().createFromPreloaded(preloader.getRootTasks()); } public ChangeData getChangeData() { - return parent == null ? TaskTree.this.changeData : parent.getChangeData(); + return TaskTree.this.changeData; } - protected Properties.Task getProperties() { - return Properties.Task.EMPTY_PARENT; + protected boolean isTrusted() { + return true; } - } - protected class Root extends NodeList { - public List<Node> getRootNodes() throws ConfigInvalidException, IOException { - if (nodes == null) { - nodes = new ArrayList<>(); - addSubDefinitions(getRootDefinitions()); + protected class SubNodeFactory { + protected Set<String> names = new HashSet<>(); + + public List<Node> createFromPreloaded(List<Task> defs) { + List<Node> nodes = new ArrayList<>(); + for (Task def : defs) { + nodes.add(createFromPreloaded(def)); + } + return nodes; } - return nodes; - } - protected List<Task> getRootDefinitions() throws ConfigInvalidException, IOException { - return taskFactory.getRootConfig().getRootTasks(); + public Node createFromPreloaded(Task def) { + return createFromPreloaded(def, (parent, definition) -> new Node(parent, definition)); + } + + public Node createFromPreloaded(Task def, ChangeData changeData) { + return createFromPreloaded( + def, + (parent, definition) -> + new Node(parent, definition) { + @Override + public ChangeData getChangeData() { + return changeData; + } + + @Override + public boolean isChange() { + return true; + } + }); + } + + protected Node createFromPreloaded(Task def, NodeFactory nodeFactory) { + if (def != null) { + try { + Node node = cachedNodeByTask.get(def.key()); + boolean isRefreshNeeded = node != null; + if (node == null) { + node = nodeFactory.create(NodeList.this, def); + } + + if (names.add(def.name())) { + // names check above detects duplicate subtasks + if (isRefreshNeeded) { + node.refreshTask(); + } + return node; + } + } catch (Exception e) { + } + } + return createInvalid(); + } + + protected Node createInvalid() { + return new Node().new Invalid(); + } } } public class Node extends NodeList { - public final Task task; - protected final Properties.Task properties; + public class Invalid extends Node { + @Override + public void refreshTask() {} - public Node(NodeList parent, Task definition) throws ConfigInvalidException, StorageException { - this.parent = parent; - this.task = definition; - this.path.addAll(parent.path); - this.path.add(definition.name); - Preloader.preload(definition); - properties = new Properties.Task(getChangeData(), definition, parent.getProperties()); + @Override + public Task getDefinition() { + return null; + } } - public List<Node> getSubNodes() { - if (nodes == null) { - nodes = new ArrayList<>(); - addSubDefinitions(); + public Task task; + public boolean isDuplicate; + + protected Properties.Statistics propertiesStatistics; + protected final Properties properties; + protected final TaskKey taskKey; + protected StatisticsMap<BranchNameKey, List<Node>> nodesByBranch; + protected boolean hasUnfilterableSubNodes = false; + + protected Node() { // Only for Invalid + taskKey = null; + properties = null; + } + + public Node(NodeList parent, Task task) { + this.parent = parent; + taskKey = task.key(); + properties = new Properties(this, task); + refreshTask(); + } + + public String key() { + return String.valueOf(getChangeData().getId().get()) + TaskConfig.SEP + taskKey; + } + + public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException { + if (cachedNodes != null) { + return refresh(cachedNodes); + } + List<Node> nodes = loadSubNodes(); + if (!properties.isSubNodeReloadRequired()) { + if (!isChange()) { + return cachedNodes = nodes; + } + definitionsBySubSection.computeIfAbsentTimed( + task.key().subSection(), + k -> nodes.stream().map(n -> n.getDefinition()).collect(toList()), + task.isVisible); + } else { + hasUnfilterableSubNodes = true; + cachedNodeByTask.clear(); + nodes.stream() + .filter(n -> !(n instanceof Invalid) && !n.isChange()) + .forEach(n -> cachedNodeByTask.put(n.task.key(), n)); } return nodes; } - protected void addSubDefinitions() throws StorageException { - addSubTaskDefinitions(); - addSubTasksFactoryDefinitions(); - addSubFileDefinitions(); - addExternalDefinitions(); - } - - protected void addSubTaskDefinitions() { - for (String name : task.subTasks) { - try { - Task def = task.config.getTaskOptional(name); - if (def != null) { - addSubDefinition(def); - } - } catch (ConfigInvalidException e) { - addSubDefinition(null); - } - } - } - - protected void addSubFileDefinitions() { - for (String file : task.subTasksFiles) { - try { - addSubDefinitions(getTaskDefinitions(task.config.getBranch(), file)); - } catch (ConfigInvalidException | IOException e) { - addSubDefinition(null); - } - } - } - - protected void addExternalDefinitions() throws StorageException { - for (String external : task.subTasksExternals) { - try { - External ext = task.config.getExternal(external); - if (ext == null) { - addSubDefinition(null); - } else { - addSubDefinitions(getTaskDefinitions(ext)); - } - } catch (ConfigInvalidException | IOException e) { - addSubDefinition(null); - } - } - } - - protected void addSubTasksFactoryDefinitions() throws StorageException { - for (String taskFactoryName : task.subTasksFactories) { - TasksFactory tasksFactory = task.config.getTasksFactory(taskFactoryName); - if (tasksFactory != null) { - NamesFactory namesFactory = task.config.getNamesFactory(tasksFactory.namesFactory); - if (namesFactory != null && namesFactory.type != null) { - new Properties.NamesFactory(namesFactory, getProperties()); - switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) { - case STATIC: - addStaticTypeTasksDefinitions(tasksFactory, namesFactory); - continue; - case CHANGE: - addChangesTypeTaskDefinitions(tasksFactory, namesFactory); - continue; - } - } - } - addSubDefinition(null); - } - } - - protected void addStaticTypeTasksDefinitions( - TasksFactory tasksFactory, NamesFactory namesFactory) { - for (String name : namesFactory.names) { - addSubDefinition(task.config.createTask(tasksFactory, name)); - } - } - - protected void addChangesTypeTaskDefinitions( - TasksFactory tasksFactory, NamesFactory namesFactory) { - try { - if (namesFactory.changes != null) { - List<ChangeData> changeDataList = - changeQueryProcessorProvider - .get() - .query(changeQueryBuilderProvider.get().parse(namesFactory.changes)) - .entities(); - for (ChangeData changeData : changeDataList) { - addSubDefinition( - task.config.createTask(tasksFactory, changeData.getId().toString()), - new ChangeNodeFactory(changeData)::createChangeNodeOrNull); - } - return; - } - } catch (StorageException e) { - log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes); - } catch (QueryParseException e) { - } - addSubDefinition(null); - } - - protected List<Task> getTaskDefinitions(External external) - throws ConfigInvalidException, IOException, StorageException { - return getTaskDefinitions(resolveUserBranch(external.user), external.file); - } - - protected List<Task> getTaskDefinitions(BranchNameKey branch, String file) - throws ConfigInvalidException, IOException { - return taskFactory - .getTaskConfig(branch, resolveTaskFileName(file), task.isTrusted) - .getTasks(); + public List<Node> getApplicableSubNodes() + throws IOException, StorageException, ConfigInvalidException { + return hasUnfilterableSubNodes ? getSubNodes() : new ApplicableNodeFilter().getSubNodes(); } @Override - protected Properties.Task getProperties() { - return properties; + protected List<Node> loadSubNodes() + throws IOException, StorageException, ConfigInvalidException { + List<Task> cachedDefinitions = definitionsBySubSection.get(task.key().subSection()); + if (cachedDefinitions != null) { + return new SubNodeFactory().createFromPreloaded(cachedDefinitions); + } + List<Node> nodes = new SubNodeAdder().getSubNodes(); + properties.expansionComplete(); + return nodes; } - protected String resolveTaskFileName(String file) throws ConfigInvalidException { - if (file == null) { - throw new ConfigInvalidException("External file not defined"); + /* The task needs to be refreshed before a node is used, however + subNode refreshing can wait until they are fetched since they may + not be needed. */ + public void refreshTask() { + this.path = new LinkedList<>(parent.path); + String key = key(); + isDuplicate = path.contains(key); + path.add(key); + + if (statistics != null) { + properties.setStatisticsConsumer( + s -> statistics.properties = (propertiesStatistics = s).sum(statistics.properties)); } - Path p = Paths.get(TASK_DIR, file); - if (!p.startsWith(TASK_DIR)) { - throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file); + this.task = properties.getTask(getChangeData()); + + this.duplicateKeys = new LinkedList<>(parent.duplicateKeys); + if (task.duplicateKey != null) { + isDuplicate |= duplicateKeys.contains(task.duplicateKey); + duplicateKeys.add(task.duplicateKey); } - return p.toString(); } - protected BranchNameKey resolveUserBranch(String user) - throws ConfigInvalidException, IOException, StorageException { - if (user == null) { - throw new ConfigInvalidException("External user not defined"); + public Properties getParentProperties() { + return (parent instanceof Node) ? ((Node) parent).properties : Properties.EMPTY; + } + + @Override + protected boolean isTrusted() { + return parent.isTrusted() && !task.isMasqueraded; + } + + @Override + public ChangeData getChangeData() { + return parent.getChangeData(); + } + + public Task getDefinition() { + return properties.isTaskRefreshRequired() ? properties.origTask : task; + } + + public boolean isChange() { + return false; + } + + public boolean match(String query) throws StorageException, QueryParseException { + return matchCache.match(getChangeData(), query, task.isVisible); + } + + public Boolean matchOrNull(String query) { + return matchCache.matchOrNull(getChangeData(), query, task.isVisible); + } + + protected class SubNodeAdder { + protected List<Node> nodes = new ArrayList<>(); + protected SubNodeFactory factory = new SubNodeFactory(); + + public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException { + addSubTasks(); + addSubTasksFactoryTasks(); + addSubTasksFiles(); + addSubTasksExternals(); + return nodes; } - Account.Id acct; - try { - acct = accountResolver.resolve(user).asUnique().account().id(); - } catch (UnprocessableEntityException e) { - throw new ConfigInvalidException("Cannot resolve user: " + user); + + protected void addSubTasks() throws IOException, StorageException { + for (ConfigSourcedValue configSourcedValue : task.subTasks) { + try { + Optional<Task> def = + preloader.getOptionalTask( + taskExpressionFactory.create( + configSourcedValue.sourceFile(), configSourcedValue.value())); + if (def.isPresent()) { + addPreloaded(def.get()); + } + } catch (ConfigInvalidException e) { + addInvalidNode(); + } + } } - return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct)); + + protected void addSubTasksFiles() { + for (ConfigSourcedValue configSourcedValue : task.subTasksFiles) { + try { + addPreloaded( + preloader.getTasks( + FileKey.create( + configSourcedValue.sourceFile().branch(), + resolveTaskFileName(configSourcedValue.value())))); + } catch (ConfigInvalidException | IOException e) { + addInvalidNode(); + } + } + } + + protected void addSubTasksExternals() throws StorageException { + for (ConfigSourcedValue configSourcedValue : task.subTasksExternals) { + try { + External ext = + taskConfigCache + .getTaskConfig(configSourcedValue.sourceFile()) + .getExternal(configSourcedValue.value()); + if (ext == null) { + addInvalidNode(); + } else { + addPreloaded(getPreloadedTasks(ext)); + } + } catch (ConfigInvalidException | IOException e) { + addInvalidNode(); + } + } + } + + protected void addSubTasksFactoryTasks() + throws IOException, StorageException, ConfigInvalidException { + for (ConfigSourcedValue configSourcedValue : task.subTasksFactories) { + TasksFactory tasksFactory = + taskConfigCache + .getTaskConfig(configSourcedValue.sourceFile()) + .getTasksFactory(configSourcedValue.value()); + if (tasksFactory != null) { + NamesFactory namesFactory = + taskConfigCache + .getTaskConfig(configSourcedValue.sourceFile()) + .getNamesFactory(tasksFactory.namesFactory); + if (namesFactory != null && namesFactory.type != null) { + namesFactory = properties.getNamesFactory(namesFactory); + switch (NamesFactoryType.getNamesFactoryType(namesFactory.type)) { + case STATIC: + addStaticTypeTasks(tasksFactory, namesFactory); + continue; + case CHANGE: + addChangeTypeTasks(tasksFactory, namesFactory); + continue; + } + } + } + addInvalidNode(); + } + } + + protected void addStaticTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory) + throws IOException, StorageException { + for (String name : namesFactory.names) { + if (StringUtils.isEmptyOrNull(name)) { + addInvalidNode(); + } else { + try { + addPreloaded(preloader.preload(task.config.new Task(tasksFactory, name))); + } catch (ConfigInvalidException e) { + addInvalidNode(); + } + } + } + } + + protected void addChangeTypeTasks(TasksFactory tasksFactory, NamesFactory namesFactory) + throws IOException { + try { + if (namesFactory.changes != null) { + for (ChangeData changeData : query(namesFactory.changes, task.isVisible)) { + addPreloaded( + preloader.preload( + task.config.new Task(tasksFactory, changeData.getId().toString())), + changeData); + } + return; + } + } catch (StorageException e) { + log.atSevere().withCause(e).log("Running changes query '%s' failed", namesFactory.changes); + } catch (QueryParseException | ConfigInvalidException e) { + } + addInvalidNode(); + } + + public void addPreloaded(List<Task> defs) { + nodes.addAll(factory.createFromPreloaded(defs)); + } + + public void addPreloaded(Task def, ChangeData changeData) { + nodes.add(factory.createFromPreloaded(def, changeData)); + } + + public void addPreloaded(Task def) { + nodes.add(factory.createFromPreloaded(def)); + } + + public void addInvalidNode() { + nodes.add(factory.createInvalid()); + } + + protected List<Task> getPreloadedTasks(External external) + throws ConfigInvalidException, IOException, StorageException { + return preloader.getTasks( + FileKey.create(resolveUserBranch(external.user), resolveTaskFileName(external.file))); + } + } + + public class ApplicableNodeFilter { + protected BranchNameKey branch = getChangeData().change().getDest(); + protected SubSectionKey subSection = task.key.subSection(); + protected Map<BranchNameKey, List<Task>> definitionsByBranch = + definitionsByBranchBySubSection.get(subSection); + + public ApplicableNodeFilter() throws StorageException {} + + public List<Node> getSubNodes() throws IOException, StorageException, ConfigInvalidException { + if (nodesByBranch != null) { + List<Node> nodes = nodesByBranch.get(branch); + if (nodes != null) { + return refresh(nodes); + } + } + if (definitionsByBranch != null) { + List<Task> branchDefinitions = definitionsByBranch.get(branch); + if (branchDefinitions != null) { + return new SubNodeFactory().createFromPreloaded(branchDefinitions); + } + } + List<Node> nodes = Node.this.getSubNodes(); + if (isChange() + && definitionsByBranch == null + && definitionsByBranchBySubSection.containsKey(subSection)) { + hasUnfilterableSubNodes = true; + } + + if (!hasUnfilterableSubNodes && !nodes.isEmpty()) { + Optional<List<Node>> filterable = getOptionalApplicableForBranch(nodes); + if (filterable.isPresent()) { + if (!isChange()) { + if (nodesByBranch == null) { + nodesByBranch = initStatistics(new HitHashMapOfCollection<>()); + } + nodesByBranch.put(branch, filterable.get()); + } else { + if (definitionsByBranch == null) { + definitionsByBranch = initStatistics(new HitHashMap<>()); + definitionsByBranchBySubSection.put(subSection, definitionsByBranch); + } + definitionsByBranch.put( + branch, + filterable.get().stream().map(node -> node.getDefinition()).collect(toList())); + } + return filterable.get(); + } + hasUnfilterableSubNodes = true; + if (isChange()) { + definitionsByBranchBySubSection.put(subSection, null); + } + } + return nodes; + } + + protected Optional<List<Node>> getOptionalApplicableForBranch(List<Node> nodes) + throws StorageException { + int filterable = 0; + List<Node> applicableNodes = new ArrayList<>(); + for (Node node : nodes) { + if (node instanceof Invalid) { + filterable++; + } else if (isApplicableCacheableByBranch(node)) { + filterable++; + try { + if (!node.match(node.task.applicable)) { + // Correctness will not be affected if more nodes are added than necessary + // (i.e. if isApplicableCacheableByBranch() does not realize a Node is cacheable + // based on its Branch), but it is incorrect to filter out a Node now that could + // later be applicable when a property, other than its Change's destination, is + // altered. + continue; + } + } catch (QueryParseException e) { + } + } + applicableNodes.add(node); + } + // Simple heuristic to determine whether storing the filtered nodes is worth it. There + // is minor evidence to suggest that storing a large list actually hurts performance. + return (filterable > nodes.size() / 2) ? Optional.of(applicableNodes) : Optional.empty(); + } + + protected boolean isApplicableCacheableByBranch(Node node) { + String applicable = node.task.applicable; + if (node.properties.isApplicableRefreshRequired()) { + return false; + } + try { + return predicateCache.isCacheableByBranch(applicable, task.isVisible); + } catch (QueryParseException e) { + return false; + } + } } } - public class ChangeNodeFactory { - public class ChangeNode extends Node { - public ChangeNode(NodeList parent, Task definition) throws ConfigInvalidException { - super(parent, definition); - } - - public ChangeData getChangeData() { - return ChangeNodeFactory.this.changeData; - } + protected String resolveTaskFileName(String file) throws ConfigInvalidException { + if (file == null) { + throw new ConfigInvalidException("External file not defined"); } - - protected ChangeData changeData; - - public ChangeNodeFactory(ChangeData changeData) { - this.changeData = changeData; + Path p = Paths.get(TASK_DIR, file); + if (!p.startsWith(TASK_DIR)) { + throw new ConfigInvalidException("task file not under " + TASK_DIR + " directory: " + file); } + return p.toString(); + } - public ChangeNode createChangeNodeOrNull(NodeList parent, Task definition) { - try { - return new ChangeNode(parent, definition); - } catch (Exception e) { - return null; + protected BranchNameKey resolveUserBranch(String user) + throws ConfigInvalidException, IOException, StorageException { + if (user == null) { + throw new ConfigInvalidException("External user not defined"); + } + Account.Id acct; + try { + acct = accountResolver.resolve(user).asUnique().account().id(); + } catch (UnprocessableEntityException e) { + throw new ConfigInvalidException("Cannot resolve user: " + user); + } + return BranchNameKey.create(allUsers.get(), RefNames.refsUsers(acct)); + } + + @SuppressWarnings("try") + public List<ChangeData> query(String query, boolean isVisible) + throws StorageException, QueryParseException { + List<ChangeData> changeDataList = changesByNamesFactoryQuery.get(query); + if (changeDataList == null) { + try (StopWatch stopWatch = + changesByNamesFactoryQuery.createLoadingStopWatch(query, isVisible)) { + changeDataList = + changeQueryProcessorProvider + .get() + .query(changeQueryBuilderProvider.get().parse(query)) + .entities(); } + changesByNamesFactoryQuery.put(query, changeDataList); } + return changeDataList; + } + + public void initStatistics(int summaryCount) { + statistics = new Statistics(); + statistics.summaryCount = summaryCount; + definitionsBySubSection.initStatistics(summaryCount); + definitionsByBranchBySubSection.initStatistics(summaryCount); + changesByNamesFactoryQuery.initStatistics(summaryCount); + } + + protected <T extends TracksStatistics> T initStatistics(T tracker) { + if (statistics != null) { + tracker.initStatistics(statistics.summaryCount); + } + return tracker; + } + + public Statistics getStatistics() { + if (statistics != null) { + statistics.definitionsPerSubSectionCache = definitionsBySubSection.getStatistics(); + statistics.definitionsByBranchBySubSectionCache = + definitionsByBranchBySubSection.getStatistics(); + statistics.changesByNamesFactoryQueryCache = changesByNamesFactoryQuery.getStatistics(); + } + return statistics; + } + + protected static List<Node> refresh(List<Node> nodes) { + for (Node node : nodes) { + node.refreshTask(); + } + return nodes; } }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java new file mode 100644 index 0000000..1fd1e3a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/ViewPathsCapability.java
@@ -0,0 +1,26 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.gerrit.extensions.config.CapabilityDefinition; + +public class ViewPathsCapability extends CapabilityDefinition { + public static final String VIEW_PATHS = "viewTaskPaths"; + + @Override + public String getDescription() { + return "View Task Paths"; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java new file mode 100644 index 0000000..ce781d9 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/AbstractExpander.java
@@ -0,0 +1,189 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * Use to expand properties like ${property} in Strings into their values. + * + * <p>Given some property name/value associations like this: + * + * <p><code> + * "animal" -> "fox" + * "bar" -> "foo" + * "obstacle" -> "fence" + * </code> + * + * <p>a String like: <code>"The brown ${animal} jumped over the ${obstacle}."</code> + * + * <p>will expand to: <code>"The brown fox jumped over the fence."</code> This class is meant to be + * used as a building block for other full featured expanders and thus must be overriden to provide + * the name/value associations via the getValueForName() method. + */ +public abstract class AbstractExpander { + protected Consumer<Matcher.Statistics> statisticsConsumer; + + protected final Map<Class<?>, Function<?, ?>> expanderByClass = new HashMap<>(); + + protected AbstractExpander() { + registerClassExpander(String.class, this::expandText); + } + + public <T> void registerClassExpander(Class<? extends T> classType, Function<T, T> expander) { + expanderByClass.put(classType, expander); + } + + public void setStatisticsConsumer(Consumer<Matcher.Statistics> statisticsConsumer) { + this.statisticsConsumer = statisticsConsumer; + } + + /** + * Returns expanded object if property found in the Strings in the object's Fields (except the + * excluded ones). Returns same object if no expansions occurred. + */ + public <C extends Cloneable> C expand(C object, Set<String> excludedFieldNames) { + return expand(new CopyOnWrite.CloneOnWrite<>(object), excludedFieldNames); + } + + /** + * Returns expanded object if property found in the Strings in the object's Fields (except the + * excluded ones). Returns same object if no expansions occurred. + */ + public <T> T expand(T object, Function<T, T> copier, Set<String> excludedFieldNames) { + return expand(new CopyOnWrite<>(object, copier), excludedFieldNames); + } + + /** + * Returns expanded object if property found in the Strings in the object's Fields (except the + * excluded ones). Returns same object if no expansions occurred. + */ + public <T> T expand(CopyOnWrite<T> cow, Set<String> excludedFieldNames) { + for (Field field : cow.getOriginal().getClass().getFields()) { + if (!excludedFieldNames.contains(field.getName())) { + expand(cow, field); + } + } + return cow.getForRead(); + } + + /** + * Returns expanded object if property found in the fieldName Field if it is a String, or in the + * List's Strings if it is a List. Returns same object if no expansions occurred. + */ + public <T> T expand(CopyOnWrite<T> cow, String fieldName) { + try { + return expand(cow, cow.getOriginal().getClass().getField(fieldName)); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } + + /** + * Returns expanded object if property found in the Field if it is a String, or in the List's + * Strings if it is a List. Returns same object if no expansions occurred. + */ + public <T> T expand(CopyOnWrite<T> cow, Field field) { + try { + field.setAccessible(true); + Object o = field.get(cow.getOriginal()); + if (o instanceof String) { + String expanded = expandText((String) o); + if (expanded != o) { + field.set(cow.getForWrite(), expanded); + } + } else if (o instanceof List) { + List<?> expanded = expand((List<?>) o); + if (expanded != o) { + field.set(cow.getForWrite(), expanded); + } + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + return cow.getForRead(); + } + + /** + * Returns expanded unmodifiable List if property found. Returns same object if no expansions + * occurred. + */ + public <T> List<T> expand(List<T> list) { + if (list != null) { + boolean hasProperty = false; + List<T> expandedList = new ArrayList<>(list.size()); + for (T value : list) { + T expanded = expand(value); + hasProperty = hasProperty || value != expanded; + expandedList.add(expanded); + } + return hasProperty ? Collections.unmodifiableList(expandedList) : list; + } + return null; + } + + /** + * Expand all properties (${property_name} -> property_value) in the given generic value. Returns + * same object if no expansions occurred. + */ + public <T> T expand(T value) { + if (value == null) { + return null; + } + @SuppressWarnings("unchecked") + Function<T, T> expander = + (Function<T, T>) expanderByClass.getOrDefault(value.getClass(), Function.identity()); + return expander.apply(value); + } + + /** + * Expand all properties (${property_name} -> property_value) in the given text. Returns same + * object if no expansions occurred. + */ + public String expandText(String text) { + if (text == null) { + return null; + } + Matcher m = new Matcher(text); + m.setStatisticsConsumer(statisticsConsumer); + if (!m.find()) { + return text; + } + StringBuffer out = new StringBuffer(); + do { + m.appendValue(out, getValueForName(m.getName())); + } while (m.find()); + m.appendTail(out); + return out.toString(); + } + + /** + * Get the replacement value for the property identified by name + * + * @param name of the property to get the replacement value for + * @return the replacement value. Since the expandText() method alwyas needs a String to replace + * '${property-name}' reference with, even when the property does not exist, this will never + * return null, instead it will returns the empty string if the property is not found. + */ + protected abstract String getValueForName(String name); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java new file mode 100644 index 0000000..d1510fa --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/CopyOnWrite.java
@@ -0,0 +1,103 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.LongConsumer; + +public class CopyOnWrite<T> { + public static class CloneOnWrite<C extends Cloneable> extends CopyOnWrite<C> { + public CloneOnWrite(C cloneable) { + super(cloneable, copier(cloneable)); + } + } + + public static <C extends Cloneable> Function<C, C> copier(C cloneable) { + return c -> clone(c); + } + + @SuppressWarnings("unchecked") + public static <C extends Cloneable> C clone(C cloneable) { + try { + for (Class<?> cls = cloneable.getClass(); cls != null; cls = cls.getSuperclass()) { + Optional<Method> optional = getOptionalDeclaredMethod(cls, "clone"); + if (optional.isPresent()) { + Method clone = optional.get(); + clone.setAccessible(true); + return (C) cloneable.getClass().cast(clone.invoke(cloneable)); + } + } + throw new RuntimeException("Cannot find clone() method"); + } catch (SecurityException + | IllegalAccessException + | IllegalArgumentException + | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + /** + * A faster getDeclaredMethod() without exceptions. The original apparently does a linear search + * anyway, and it is significantly slower when it throws NoSuchMethodExceptions. + */ + public static Optional<Method> getOptionalDeclaredMethod( + Class<?> cls, String name, Class<?>... parameterTypes) { + for (Method method : cls.getDeclaredMethods()) { + if (method.getName().equals(name) + && Arrays.equals(method.getParameterTypes(), parameterTypes)) { + return Optional.of(method); + } + } + return Optional.empty(); + } + + protected Function<T, T> copier; + protected StopWatch.Runner stopWatch = StopWatch.Runner.DISABLED; + protected T original; + protected T copy; + + public CopyOnWrite(T original, Function<T, T> copier) { + this.original = original; + this.copier = copier; + } + + protected void setNanosecondsConsumer(LongConsumer nanosConsumer) { + stopWatch = new StopWatch.Runner.Enabled().setNanosConsumer(nanosConsumer); + } + + public T getOriginal() { + return original; + } + + public T getForRead() { + return isCopy() ? copy : original; + } + + public T getForWrite() { + if (!isCopy()) { + stopWatch.run(() -> copy = copier.apply(original)); + } + return copy; + } + + public boolean isCopy() { + return copy != null; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java new file mode 100644 index 0000000..df817a1 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Expander.java
@@ -0,0 +1,89 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +/** + * Use to expand properties whose values may contain other references to properties. + * + * <p>Using a recursive expansion approach makes order of evaluation unimportant as long as there + * are no looping definitions. + * + * <p>Given some property name/value asssociations defined like this: + * + * <p><code> + * valueByName.put("obstacle", "fence"); + * valueByName.put("action", "jumped over the ${obstacle}"); + * </code> + * + * <p>a String like: <code>"The brown fox ${action}."</code> + * + * <p>will expand to: <code>"The brown fox jumped over the fence."</code> + */ +public class Expander extends AbstractExpander { + protected final Function<String, String> loadingFunction; + protected final Map<String, String> valueByName = new HashMap<>(); + protected final Set<String> expanding = new HashSet<>(); + + public Expander(Function<String, String> loadingFunction) { + this.loadingFunction = loadingFunction; + } + + /** + * Expand all properties (${property_name} -> property_value) in the given text. Returns same + * object if no expansions occurred. + */ + public Map<String, String> expand(Map<String, String> map) { + if (map != null) { + boolean hasProperty = false; + Map<String, String> expandedMap = new HashMap<>(map.size()); + for (Map.Entry<String, String> e : map.entrySet()) { + String name = e.getKey(); + String value = e.getValue(); + String expanded = getValueForName(name); + hasProperty = hasProperty || value != expanded; + expandedMap.put(name, expanded); + } + return hasProperty ? Collections.unmodifiableMap(expandedMap) : map; + } + return null; + } + + @Override + public String getValueForName(String name) { + String value = valueByName.get(name); + if (value != null) { + return value; + } + value = loadingFunction.apply(name); + if (value == null) { + value = ""; + } else if (!value.isEmpty()) { + if (!expanding.add(name)) { + throw new RuntimeException("Looping property definitions."); + } + value = expandText(value); + expanding.remove(name); + } + valueByName.put(name, value); + return value; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java new file mode 100644 index 0000000..05120b3 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Loader.java
@@ -0,0 +1,93 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import com.google.gerrit.entities.Change; +import com.google.gerrit.exceptions.StorageException; +import com.google.gerrit.server.query.change.ChangeData; +import com.googlesource.gerrit.plugins.task.TaskConfig.Task; +import java.util.function.Function; + +public class Loader { + protected final Task task; + protected final ChangeData changeData; + protected final Function<String, String> inherritedMapper; + protected Change change; + protected boolean isInheritedPropertyLoaded; + + public Loader(Task task, ChangeData changeData, Function<String, String> inherritedMapper) { + this.task = task; + this.changeData = changeData; + this.inherritedMapper = inherritedMapper; + } + + public boolean isNonTaskDefinedPropertyLoaded() { + return change != null || isInheritedPropertyLoaded; + } + + public String load(String name) throws StorageException { + if (name.startsWith("_")) { + return internal(name); + } + String value = task.exported.get(name); + if (value == null) { + value = task.properties.get(name); + if (value == null) { + value = inherritedMapper.apply(name); + if (!value.isEmpty()) { + isInheritedPropertyLoaded = true; + } + } + } + return value; + } + + protected String internal(String name) throws StorageException { + if ("_name".equals(name)) { + return task.name(); + } + String changeProp = name.replace("_change_", ""); + if (changeProp != name) { + return change(changeProp); + } + return ""; + } + + protected String change(String changeProp) throws StorageException { + switch (changeProp) { + case "number": + return String.valueOf(change().getId().get()); + case "id": + return change().getKey().get(); + case "project": + return change().getProject().get(); + case "branch": + return change().getDest().branch(); + case "status": + return change().getStatus().toString(); + case "topic": + return change().getTopic(); + default: + return ""; + } + } + + protected Change change() { + if (change == null) { + change = changeData.change(); + } + return change; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java new file mode 100644 index 0000000..68151fb --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Matcher.java
@@ -0,0 +1,103 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; +import java.util.function.Consumer; + +/** A handcrafted properties Matcher which has an API similar to an RE Matcher, but is faster. */ +public class Matcher { + public static class Statistics { + public long appendNanoseconds; + public long findNanoseconds; + + public Statistics sum(Statistics other) { + if (other == null) { + return this; + } + Statistics statistics = new Statistics(); + statistics.appendNanoseconds = appendNanoseconds + other.appendNanoseconds; + statistics.findNanoseconds = findNanoseconds + other.findNanoseconds; + return statistics; + } + } + + protected String text; + protected int start; + protected int nameStart; + protected int end; + protected int cursor; + + protected Statistics statistics; + protected StopWatch.Runner appendNanoseconds = StopWatch.Runner.DISABLED; + protected StopWatch.Runner findNanoseconds = StopWatch.Runner.DISABLED; + + public Matcher(String text) { + this.text = text; + } + + protected void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) { + if (statisticsConsumer != null) { + statistics = new Statistics(); + statisticsConsumer.accept(statistics); + appendNanoseconds = + new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.appendNanoseconds = ns); + findNanoseconds = + new StopWatch.Runner.Enabled().setNanosConsumer(ns -> statistics.findNanoseconds = ns); + } + } + + public boolean find() { + return findNanoseconds.get(() -> findUntimed()); + } + + protected boolean findUntimed() { + start = text.indexOf("${", cursor); + nameStart = start + 2; + if (start < 0 || text.length() < nameStart + 1) { + return false; + } + end = text.indexOf('}', nameStart); + boolean found = end >= 0; + return found; + } + + public String getName() { + return text.substring(nameStart, end); + } + + public void appendValue(StringBuffer buffer, String value) { + appendNanoseconds.accept((b, v) -> appendValueUntimed(b, v), buffer, value); + } + + protected void appendValueUntimed(StringBuffer buffer, String value) { + if (start > cursor) { + buffer.append(text.substring(cursor, start)); + } + buffer.append(value); + cursor = end + 1; + } + + public void appendTail(StringBuffer buffer) { + appendNanoseconds.accept(b -> appendTailUntimed(b), buffer); + } + + protected void appendTailUntimed(StringBuffer buffer) { + if (cursor < text.length()) { + buffer.append(text.substring(cursor)); + cursor = text.length(); + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java new file mode 100644 index 0000000..6177f2b --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/properties/Properties.java
@@ -0,0 +1,170 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.properties; + +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.server.query.change.ChangeData; +import com.googlesource.gerrit.plugins.task.ConfigSourcedValue; +import com.googlesource.gerrit.plugins.task.TaskConfig; +import com.googlesource.gerrit.plugins.task.TaskConfig.NamesFactory; +import com.googlesource.gerrit.plugins.task.TaskConfig.Task; +import com.googlesource.gerrit.plugins.task.TaskTree; +import com.googlesource.gerrit.plugins.task.statistics.StopWatch; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; + +/** Use to expand properties like ${_name} in the text of various definitions. */ +public class Properties { + public static class Statistics { + public long getTaskNanoseconds; + public long copierNanoseconds; + public Matcher.Statistics matcher; + + public static void setNanoseconds(Statistics stats, long nanos) { + if (stats != null) { + stats.getTaskNanoseconds = nanos; + } + } + + public Statistics sum(Statistics other) { + if (other == null) { + return this; + } + Statistics statistics = new Statistics(); + statistics.getTaskNanoseconds = getTaskNanoseconds + other.getTaskNanoseconds; + statistics.copierNanoseconds = copierNanoseconds + other.copierNanoseconds; + statistics.matcher = matcher == null ? other.matcher : matcher.sum(other.matcher); + return statistics; + } + } + + public static final Properties EMPTY = + new Properties() { + @Override + protected Function<String, String> getParentMapper() { + return n -> ""; + } + }; + + public final Task origTask; + protected final TaskTree.Node node; + protected final CopyOnWrite<Task> task; + protected Statistics statistics; + protected Consumer<Statistics> statisticsConsumer; + protected Consumer<Matcher.Statistics> matcherStatisticsConsumer; + protected Expander expander; + protected Loader loader; + protected boolean init = true; + protected boolean isTaskRefreshRequired; + protected boolean isApplicableRefreshRequired; + protected boolean isSubNodeReloadRequired; + + public Properties() { + this(null, null); + expander = new Expander(n -> ""); + } + + public Properties(TaskTree.Node node, Task origTask) { + this.node = node; + this.origTask = origTask; + task = new CopyOnWrite.CloneOnWrite<>(origTask); + } + + /** Use to expand properties specifically for Tasks. */ + @SuppressWarnings("try") + public Task getTask(ChangeData changeData) { + try (StopWatch stopWatch = + StopWatch.builder() + .enabled(statistics != null) + .build() + .setNanosConsumer(l -> Statistics.setNanoseconds(statistics, l))) { + loader = new Loader(origTask, changeData, getParentMapper()); + expander = new Expander(n -> loader.load(n)); + expander.registerClassExpander( + ConfigSourcedValue.getClassType(), getConfigSourcedValueExpander(expander)); + expander.setStatisticsConsumer(matcherStatisticsConsumer); + if (isTaskRefreshRequired || init) { + expander.expand(task, TaskConfig.KEY_APPLICABLE); + isApplicableRefreshRequired = loader.isNonTaskDefinedPropertyLoaded(); + + expander.expand(task, ImmutableSet.of(TaskConfig.KEY_APPLICABLE, TaskConfig.KEY_NAME)); + + Map<String, String> exported = expander.expand(origTask.exported); + if (exported != origTask.exported) { + task.getForWrite().exported = exported; + } + + if (init) { + init = false; + isTaskRefreshRequired = loader.isNonTaskDefinedPropertyLoaded(); + } + } + } + if (statisticsConsumer != null) { + statisticsConsumer.accept(statistics); + } + return task.getForRead(); + } + + protected Function<ConfigSourcedValue, ConfigSourcedValue> getConfigSourcedValueExpander( + Expander expander) { + return t -> { + String toExpand = t.value(); + String expanded = expander.expandText(toExpand); + if (toExpand != expanded) { + return ConfigSourcedValue.create(t.sourceFile(), expanded); + } + return t; + }; + } + + public void setStatisticsConsumer(Consumer<Statistics> statisticsConsumer) { + if (statisticsConsumer != null) { + this.statisticsConsumer = statisticsConsumer; + statistics = new Statistics(); + matcherStatisticsConsumer = s -> statistics.matcher = s; + task.setNanosecondsConsumer(ns -> statistics.copierNanoseconds = ns); + } + } + + // To detect NamesFactories dependent on non task defined properties, the checking must be + // done after subnodes are fully loaded, which unfortunately happens after getTask() is + // called, therefore this must be called after all subnodes have been loaded. + public void expansionComplete() { + isSubNodeReloadRequired = loader.isNonTaskDefinedPropertyLoaded(); + } + + public boolean isApplicableRefreshRequired() { + return isApplicableRefreshRequired; + } + + public boolean isTaskRefreshRequired() { + return isTaskRefreshRequired; + } + + public boolean isSubNodeReloadRequired() { + return isSubNodeReloadRequired; + } + + /** Use to expand properties specifically for NamesFactories. */ + public NamesFactory getNamesFactory(NamesFactory namesFactory) { + return expander.expand(namesFactory, ImmutableSet.of(TaskConfig.KEY_TYPE)); + } + + protected Function<String, String> getParentMapper() { + return n -> node.getParentProperties().expander.getValueForName(n); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java new file mode 100644 index 0000000..2f35084 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitBooleanTable.java
@@ -0,0 +1,94 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +import com.google.gerrit.common.BooleanTable; +import com.googlesource.gerrit.plugins.task.util.TopKeyMap; + +/** + * A space efficient Table for Booleans. This Table takes advantage of the fact that the values + * stored in it are all Booleans and uses BitSets to make this very space efficient. + */ +public class HitBooleanTable<R, C> extends BooleanTable<R, C> implements TracksStatistics { + public static class Statistics<V> { + public long hits; + public long misses; + public long size; + public int numberOfRows; + public int numberOfColumns; + public Long sumNanosecondsLoading; + public TopKeyMap<V> topNanosecondsLoadingKeys; + } + + protected Statistics<TopKeyMap.TableKeyValue<R, C>> statistics; + + @Override + public Boolean get(R r, C c) { + Boolean value = super.get(r, c); + if (statistics != null) { + if (value != null) { + statistics.hits++; + } else { + statistics.misses++; + } + } + return value; + } + + public StopWatch createLoadingStopWatch(R row, C column, boolean isVisible) { + if (statistics == null) { + return StopWatch.DISABLED; + } + if (statistics.sumNanosecondsLoading == null) { + statistics.sumNanosecondsLoading = 0L; + } + return new StopWatch.Enabled() + .setNanosConsumer( + ns -> + statistics.sumNanosecondsLoading += + updateTopLoadingTimes(ns, row, column, isVisible)); + } + + public long updateTopLoadingTimes(long nanos, R row, C column, boolean isVisible) { + statistics.topNanosecondsLoadingKeys.addIfTop( + nanos, isVisible ? new TopKeyMap.TableKeyValue<R, C>(row, column) : null); + return nanos; + } + + @Override + public void initStatistics(int summaryCount) { + statistics = new Statistics<>(); + statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount); + } + + @Override + public void ensureStatistics(int summaryCount) { + if (statistics == null) { + initStatistics(summaryCount); + } + } + + @Override + public Object getStatistics() { + statistics.numberOfRows = rowByRow.size(); + statistics.numberOfColumns = positionByColumn.size(); + statistics.size = + rowByRow.values().stream() + .map(r -> (long) r.hasValues.size() + (long) r.values.size()) + .mapToLong(Long::longValue) + .sum(); + return statistics; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java new file mode 100644 index 0000000..ccb12a9 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMap.java
@@ -0,0 +1,184 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +import static java.util.stream.Collectors.toList; + +import com.googlesource.gerrit.plugins.task.util.TopKeyMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class HitHashMap<K, V> extends HashMap<K, V> implements StatisticsMap<K, V> { + public static class Statistics<K> { + public long hits; + public int size; + public Long sumNanosecondsLoading; + public TopKeyMap<K> topNanosecondsLoadingKeys; + public List<Object> elements; + } + + public static final long serialVersionUID = 1; + + protected Statistics<K> statistics; + + public HitHashMap() {} + + @Override + public V get(Object key) { + V v = super.get(key); + if (statistics != null && v != null) { + statistics.hits++; + } + return v; + } + + @Override + public V getOrDefault(Object key, V dv) { + V v = get(key); + if (v == null) { + return dv; + } + return v; + } + + @Override + @SuppressWarnings("try") + public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) { + V v = get(key); + if (v == null) { + v = mappingFunction.apply(key); + if (v != null) { + put(key, v); + } + } + return v; + } + + @Override + @SuppressWarnings("try") + public V computeIfAbsentTimed( + K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible) { + V v = get(key); + if (v == null) { + try (StopWatch stopWatch = createLoadingStopWatch(key, isVisible)) { + v = mappingFunction.apply(key); + } + if (v != null) { + put(key, v); + } + } + return v; + } + + @Override + public V put(K key, V value) { + if (statistics != null && value instanceof TracksStatistics) { + ((TracksStatistics) value).ensureStatistics(statistics.topNanosecondsLoadingKeys.size()); + } + return super.put(key, value); + } + + @Override + public void putAll(Map<? extends K, ? extends V> m) { + m.entrySet().stream().forEach(e -> put(e.getKey(), e.getValue())); + } + + @Override + public V putIfAbsent(K key, V value) { + if (!containsKey(key)) { + put(key, value); + return null; + } + return get(key); + } + + @Override + public V computeIfPresent( + K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public V replace(K key, V value) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) { + throw new UnsupportedOperationException(); // Todo if needed + } + + @Override + public void initStatistics(int summaryCount) { + statistics = new Statistics<>(); + statistics.topNanosecondsLoadingKeys = new TopKeyMap<>(summaryCount); + } + + @Override + public void ensureStatistics(int summaryCount) { + if (statistics == null) { + initStatistics(summaryCount); + } + } + + public StopWatch createLoadingStopWatch(K key, boolean isVisible) { + if (statistics == null) { + return StopWatch.DISABLED; + } + if (statistics.sumNanosecondsLoading == null) { + statistics.sumNanosecondsLoading = 0L; + } + return new StopWatch.Enabled() + .setNanosConsumer( + ns -> statistics.sumNanosecondsLoading += updateTopLoadingTimes(ns, key, isVisible)); + } + + public long updateTopLoadingTimes(long nanos, K key, boolean isVisible) { + statistics.topNanosecondsLoadingKeys.addIfTop(nanos, isVisible ? key : null); + return nanos; + } + + @Override + public Object getStatistics() { + statistics.size = size(); + List<Object> elementStatistics = + values().stream() + .filter(e -> e instanceof TracksStatistics) + .map(e -> ((TracksStatistics) e).getStatistics()) + .collect(toList()); + if (!elementStatistics.isEmpty()) { + statistics.elements = elementStatistics; + } + return statistics; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java new file mode 100644 index 0000000..31a9d61 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/HitHashMapOfCollection.java
@@ -0,0 +1,65 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +import static java.util.stream.Collectors.toList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; + +public class HitHashMapOfCollection<K, V extends Collection<?>> extends HitHashMap<K, V> { + public static class Statistics<K> extends HitHashMap.Statistics<K> { + public List<Integer> top5CollectionSizes; + public List<Integer> bottom5CollectionSizes; + } + + public static final long serialVersionUID = 1; + + protected Statistics<K> statistics; + + public HitHashMapOfCollection() {} + + @Override + public void initStatistics(int summaryCount) { + super.initStatistics(summaryCount); + statistics = new Statistics<>(); + } + + @Override + public Object getStatistics() { + super.getStatistics(); + statistics.hits = super.statistics.hits; + statistics.size = super.statistics.size; + + List<Integer> collectionSizes = + values().stream().map(l -> l.size()).sorted(Comparator.reverseOrder()).collect(toList()); + statistics.top5CollectionSizes = new ArrayList<>(5); + statistics.bottom5CollectionSizes = new ArrayList<>(5); + for (int i = 0; i < 5 && i < collectionSizes.size(); i++) { + statistics.top5CollectionSizes.add(collectionSizes.get(i)); + int bottom = collectionSizes.size() - 6 + i; + if (bottom > 4 && bottom < collectionSizes.size()) { + // The > 4 ensures that there are no entries also in the top list + statistics.bottom5CollectionSizes.add(collectionSizes.get(bottom)); + } + } + if (statistics.bottom5CollectionSizes.isEmpty()) { + statistics.bottom5CollectionSizes = null; + } + return statistics; + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java new file mode 100644 index 0000000..5bca24d --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StatisticsMap.java
@@ -0,0 +1,25 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +import java.util.Map; +import java.util.function.Function; + +public interface StatisticsMap<K, V> extends Map<K, V>, TracksStatistics { + V computeIfAbsentTimed( + K key, Function<? super K, ? extends V> mappingFunction, boolean isVisible); + + StopWatch createLoadingStopWatch(K key, boolean isVisible); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java new file mode 100644 index 0000000..05f942b --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/StopWatch.java
@@ -0,0 +1,140 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +import com.google.common.base.Stopwatch; +import com.googlesource.gerrit.plugins.task.util.SamTryWrapper; +import java.util.concurrent.TimeUnit; +import java.util.function.LongConsumer; + +/** + * StopWatches with APIs designed to make it easy to disable, and to make robust in the face of + * exceptions. + * + * <p>The Stopwatch class from google commons is used by placing start() and stop() calls around + * code sections which need to be timed. This approach can be problematic since the code being timed + * could throw an exception and if the stop() is not in a finally clause, then it will likely never + * get called, potentially causing bad timings, or worse programatic issues elsewhere due to double + * calls to start(). The need for a finally clause to make things safe is an obvious hint that using + * an AutoCloseable approach is likely going to be safer. With that in mind, the two API approaches + * provided by these StopWatch classes are: + * + * <ol> + * <li>The timed try-with-resource API. Use the StopWatch.Enabled class for this API. + * <li>The timed SAM evaluation API. Use the StopWatch.Runner.Enabled class for these APIs. + * </ol> + * + * <p>Finally, the commons stopwatch API also does not provide an easy way to disable timings at + * runtime when they are not desired, so the DISABLED classes can be used for this with both + * approaches above, and thus provide low cost runtime substitutes for either the StopWatch.Enabled + * or StopWatch.Runner.Enabled classes. + */ +public interface StopWatch extends AutoCloseable { + /** Designed for the greatest simplicity to time SAM executions. */ + public abstract static class Runner extends SamTryWrapper<AutoCloseable> { + public static class Enabled extends Runner { + protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER; + + @Override + protected AutoCloseable getAutoCloseable() { + return new StopWatch.Enabled().setNanosConsumer(nanosConsumer); + } + + @Override + public Runner setNanosConsumer(LongConsumer nanosConsumer) { + this.nanosConsumer = nanosConsumer; + return this; + } + } + + /** May be used anywhere that Enabled can be used */ + public static final Runner DISABLED = + new Runner() { + @Override + protected AutoCloseable getAutoCloseable() { + return () -> {}; + } + }; + + public Runner setNanosConsumer(LongConsumer nanosConsumer) { + return this; + } + } + + /** Should be created and used only within a try-with-resource */ + public static class Enabled implements StopWatch { + protected LongConsumer nanosConsumer = EMPTY_LONG_CONSUMER; + protected Stopwatch stopwatch = Stopwatch.createStarted(); + + @Override + public StopWatch setNanosConsumer(LongConsumer nanosConsumer) { + this.nanosConsumer = nanosConsumer; + return this; + } + + @Override + public void close() { + stopwatch.stop(); + nanosConsumer.accept(stopwatch.elapsed(TimeUnit.NANOSECONDS)); + } + } + + /** + * A easy way to build a timer which needes to be enabled/disabled based on a runtime boolean. + * + * <p>Example Usage: + * + * <p><code> + * try (StopWatch stopWatch = + * StopWatch.builder().enabled(myBoolean).build().setNanosConsumer(myConsumer)) { + * // Code to be timed here... + * } + * </code> + */ + public static class Builder { + protected static class Enabled extends Builder { + @Override + public StopWatch build() { + return new StopWatch.Enabled(); + } + } + + protected static final Builder ENABLED = new Enabled(); + protected static final Builder DISABLED = new Builder(); + + public Builder enabled(boolean enabled) { + return enabled ? ENABLED : this; + } + + public StopWatch build() { + return StopWatch.DISABLED; + } + } + + /** May be used anywhere that Enabled can be used */ + public static final StopWatch DISABLED = new StopWatch() {}; + + public static final LongConsumer EMPTY_LONG_CONSUMER = l -> {}; + + public static Builder builder() { + return Builder.DISABLED; + } + + default StopWatch setNanosConsumer(LongConsumer nanosConsumer) { + return this; + } + + default void close() {} +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java new file mode 100644 index 0000000..a150600 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/statistics/TracksStatistics.java
@@ -0,0 +1,23 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.statistics; + +public interface TracksStatistics { + void initStatistics(int summaryCount); + + void ensureStatistics(int summaryCount); + + Object getStatistics(); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java new file mode 100644 index 0000000..434a4de --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/Copier.java
@@ -0,0 +1,43 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.util; + +import java.lang.reflect.Field; + +public class Copier { + public static <T> void shallowCopyDeclaredFields( + Class<T> cls, T from, T to, boolean includeInaccessible) { + for (Field field : cls.getDeclaredFields()) { + try { + if (includeInaccessible) { + field.setAccessible(true); + } + Object val = field.get(from); + if (!field.getName().equals("this$0")) { // Can't copy internal final field + field.set(to, val); + } + } catch (IllegalAccessException e) { + if (includeInaccessible) { + throw new RuntimeException( + "Cannot access field to copy it " + fieldValueToString(field, "unknown")); + } + } + } + } + + protected static String fieldValueToString(Field field, Object val) { + return "field:" + field.getName() + " value:" + val + " type:" + field.getType(); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java new file mode 100644 index 0000000..ef42e5a --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/SamTryWrapper.java
@@ -0,0 +1,84 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.util; + +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Class designed to make SAM calls wrapped by an AutoCloseable. This is usefull with AutoCloseables + * which do not provide resources which are directly needed during the SAM call, but rather with + * AutoCloseables which likely manage external resources or state such as a locks or timers. + */ +public abstract class SamTryWrapper<A extends AutoCloseable> { + protected abstract A getAutoCloseable(); + + @SuppressWarnings("try") + public void run(Runnable runnable) { + try (A autoCloseable = getAutoCloseable()) { + runnable.run(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("try") + public <T> T get(Supplier<T> supplier) { + try (A autoCloseable = getAutoCloseable()) { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("try") + public <T> void accept(Consumer<T> consumer, T t) { + try (A autoCloseable = getAutoCloseable()) { + consumer.accept(t); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("try") + public <T, U> void accept(BiConsumer<T, U> consumer, T t, U u) { + try (A autoCloseable = getAutoCloseable()) { + consumer.accept(t, u); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("try") + public <T, R> R apply(Function<T, R> func, T t) { + try (A autoCloseable = getAutoCloseable()) { + return func.apply(t); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @SuppressWarnings("try") + public <T, U, R> R apply(BiFunction<T, U, R> func, T t, U u) { + try (A autoCloseable = getAutoCloseable()) { + return func.apply(t, u); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java similarity index 95% rename from src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java rename to src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java index 7644143..3d5197e 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/task/ThrowingProvider.java +++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/ThrowingProvider.java
@@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.googlesource.gerrit.plugins.task; +package com.googlesource.gerrit.plugins.task.util; public interface ThrowingProvider<V, E extends Exception> { public V get() throws E;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java new file mode 100644 index 0000000..a6627fb --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/task/util/TopKeyMap.java
@@ -0,0 +1,94 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task.util; + +/** + * A TopKeyMap is a lightweight limited size (default 5) map with 'long' keys designed to store only + * the elements with the top five largest keys. + * + * <p>A TopKeyMap is array based and has O(n) insertion time. Despite not having O(1) insertion + * times, it should likely be much faster than a hash based map for small n sizes. It also is more + * memory efficient than a hash based map, although both are likely O(n) in space usage. The + * TopKeyMap allocates all of its entries up front so it does not change its memory utilization at + * all, and it does not have to create or free any Objects during its post constructor lifespan. + * + * <p>While a TopKeyMap currently only uses 'long's as keys, it is possible to easiy upgrade this + * collection to use any type of Comparable key. + * + * <p>Although not currently thread safe, due to the simplicity of the data structures used, and the + * insertion approach, it is easy to make a TopKeyMap efficiently thread safe. + */ +public class TopKeyMap<V> { + /** + * A TableKeyValue is a helper class for TopKeyMap use cases, such as a table with with row and + * column keys, which involve two values. + */ + public static class TableKeyValue<R, C> { + public final R row; + public final C column; + + public TableKeyValue(R row, C column) { + this.row = row; + this.column = column; + } + } + + protected class Entry { + public long key; + public V value; + + protected void set(long key, V value) { + this.key = key; + this.value = value; + } + } + + protected Entry[] entries; + + public TopKeyMap() { + this(5); + } + + @SuppressWarnings("unchecked") + public TopKeyMap(int length) { + entries = (Entry[]) new Object[length]; + for (int i = 0; i < entries.length; i++) { + entries[i] = new Entry(); + } + } + + public void addIfTop(long key, V value) { + addIfTop(0, key, value); + } + + protected void addIfTop(int i, long key, V value) { + if (entries[entries.length - 1].key < key) { + for (; i < entries.length; i++) { + Entry e = entries[i]; + if (e.key < key) { + long eKValue = e.key; + V eValue = e.value; + e.set(key, value); + addIfTop(i + 1, eKValue, eValue); + return; + } + } + } + } + + public int size() { + return entries.length; + } +}
diff --git a/src/main/resources/Documentation/config-gerrit.md b/src/main/resources/Documentation/config-gerrit.md new file mode 100644 index 0000000..6f0b0a5 --- /dev/null +++ b/src/main/resources/Documentation/config-gerrit.md
@@ -0,0 +1,25 @@ +# Admin User Guide - Configuration + +## File `etc/gerrit.config` + +The file `'$site_path'/etc/gerrit.config` is a Git-style config file +that controls many host specific settings for Gerrit. + +### Section @PLUGIN@ "cacheable-predicates" + +The @PLUGIN@.cacheable-predicates section configures Change Predicate +optimizations which the @PLUGIN@ plugin may use when evaluating tasks. + +#### @PLUGIN@.cacheable-predicates.byBranch-className + +The value set with this key specifies a fully qualified class name +of a Predicate which can be assumed to always return the same match +result to all Changes destined for the same project/branch +combinations. This key may be specified more than once. + +Example: + +``` +[@PLUGIN@ "cacheable-predicates"] + byBranch-className = com.google.gerrit.server.query.change.BranchSetPredicate +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/task.md b/src/main/resources/Documentation/task.md index fa5e834..75ee189 100644 --- a/src/main/resources/Documentation/task.md +++ b/src/main/resources/Documentation/task.md
@@ -51,11 +51,17 @@ completes. A task with a `WAITING` status is not yet ready to execute. A task in this -state is blocked by its subtasks which are not yet in the `PASS` state. +state is blocked by its subtasks which are not yet in the `PASS` or `DUPLICATE` +state. A task with a `READY` status is ready to be executed. All of its subtasks are in the `PASS` state. +A task with a `DUPLICATE` status has the same task key as one of its ancestors. +Task keys are generally made up of the canonical task name and the change to +which it applies. To avoid infinite loops, subtasks are ignored on duplicate +tasks. + A task with a `PASS` status meets all the criteria for `READY`, and has executed and was successful. @@ -160,8 +166,8 @@ from the current task if they redefined in the current task. Attributes which are lists (such as subtasks) or maps (such as properties), will be preloaded by the preload-task and then extended with the attributes from the -current task. See [Optional Tasks](#optional_tasks) for how to define optional -preload-tasks. +current task. See [Task Expression](task_expression.html) for how to define +optional preload-tasks. Example: ``` @@ -172,8 +178,8 @@ : This key lists the name of a subtask of the current task. This key may be used several times in a task section to define more than one subtask for a -particular task. See [Optional Tasks](#optional_tasks) for how to define -optional subtasks. +particular task. See [Task Expression](task_expression.html) for how to define +subtasks. Example: @@ -231,6 +237,48 @@ subtasks-file = common.config # references the file named task/common.config ``` +`duplicate-key` + +: This key defines an identifier to help identify tasks which should be +considered duplicates even if they are not exact duplicates. When the task +plugin encounters a task with the same duplicate-key as one of its +ancestors, it will be considered a duplicate of that ancestor. Tasks such as +a starting task and a looping tasks-factory that preload the same base task +are not exact duplicates, yet they may logically represent duplicates. In +this case, defining a `duplicate-key` on the base task which is preloaded +from two different places (usually a root and a change tasks-factory), will +ensure that any loops are halted once the original change is reached. Without +a duplicate-key, the walking would generally walk one task further than +desired. + +Outlined below is a simple way to walk a change's git dependencies in the +task plugin. While Git does not allow loops in commit histories, sometimes +in Gerrit when changes get rebased, it can cause loops (because Gerrit +sometimes tracks outdated dependencies). The use of the duplicate-key +below results in the loop being detected when you would expect it to be. + +Example: + +``` +[root "git dependencies"] + applicable = status:new + preload-task = git dependencies + +[task "git dependencies"] + fail = -status:new + fail-hint = [${_change_status}] dependency needs to be OPEN + subtasks-factory = git dependencies + duplicate-key = git dependencies ${_change_number} + +[tasks-factory "git dependencies"] + names-factory = git dependencies + preload-task = git dependencies + +[names-factory "git dependencies"] + type = change + changes = -status:merged parentof:${_change_number} project:${_change_project} branch:${_change_branch} +``` + Root Tasks ---------- Root tasks typically define the "final verification" tasks for changes. Each @@ -275,26 +323,7 @@ fail = label:code-review-2 ``` -<a id="optional_tasks"/> -Optional Tasks --------------- -To define a task that may not exist and that will not cause the task referencing -it to be INVALID, follow the task name with pipe (`|`) character. This feature -is particularly useful when a property is used in the task name. - -``` - preload-task = Optional Subtask {$_name} | -``` - -To define an alternate task to load when an optional task does not exist, -list the alterante task name after the pipe (`|`) character. This feature -may be chained together as many times as needed. - -``` - subtask = Optional Subtask {$_name} | - Backup Optional Subtask {$_name} Backup | - Default Subtask # Must exist if the above two don't! -``` +<a id="tasks_factory"/> Tasks-Factory ------------- A tasks-factory section supports all the keys supported by task sections. In @@ -419,9 +448,9 @@ Examples: ``` - fail-hint = {$_name} needs to be fixed - fail-hint = {$_change_number} with {$_change_status} needs to be fixed - fail-hint = {$_change_id} on {$_change_project} and {$_change_branch} needs to be fixed + fail-hint = ${_name} needs to be fixed + fail-hint = ${_change_number} with ${_change_status} needs to be fixed + fail-hint = ${_change_id} on ${_change_project} and ${_change_branch} needs to be fixed changes = parentof:${_change_number} project:${_change_project} branch:${_change_branch} ``` @@ -509,7 +538,33 @@ not output anything. This switch is particularly useful in combination with the **\-\-@PLUGIN@\-\-preview** switch. -**\-\-@PLUGIN@\-\-task\-\-evaluation-time** +**\-\-@PLUGIN@\-\-include-paths** + +This switch will show the absolute path of each task. This is meant for +debugging when tasks are spread out in different files. A task path includes +task name, type (indicating one of root, task, tasks-factory), +[task factory](#tasks_factory) name (if it is generated by one), file name, +project, and branch the file belongs to. Additionally, if a task is on a user +ref, it also shows the identity of that user. Only users with `viewTaskPaths` +capability on the server can view absolute task paths with this switch. + +``` + $ ssh -x -p 29418 example.com gerrit query change:123 \-\-@PLUGIN@\-\-include-paths + ... + plugins: + name: task + roots: + name: Jenkins Build and Test + inProgress: false + status: READY + path: + name: Jenkins Build and Test + project: All-Projects.git + branch: refs/meta/config + file: task.config +``` + +**\-\-@PLUGIN@\-\-evaluation-time** This switch is meant as a debug switch to evaluate task performance. This switch outputs an elapsed time value on every task indicating how much time @@ -533,6 +588,13 @@ status: PASS ``` +**\-\-@PLUGIN@\-\-only** + +This switch can be used to only evaluate tasks under a certain root when tasks +from other roots are unwanted. For example, a CI system may not be interested +in evaluating tasks for another CI system. The switch can be provided multiple +times. + Examples -------- See [task_states](test/task_states.html) for a comprehensive list of examples
diff --git a/src/main/resources/Documentation/task_expression.md b/src/main/resources/Documentation/task_expression.md new file mode 100644 index 0000000..c084a39 --- /dev/null +++ b/src/main/resources/Documentation/task_expression.md
@@ -0,0 +1,220 @@ +<a id="task_expression"/> +Task Expression +-------------- + +The tasks in subtask and preload-task can be defined using a Task Expression. +Each task expression can contain multiple tasks (all can be optional). Tasks +from other files and refs can be referenced using [Task Reference](#task_reference). + +``` +TASK_EXPR = TASK_REFERENCE [ WHITE_SPACE * '|' [ WHITE_SPACE * TASK_EXPR ] ] +``` + +To define a task that may not exist and that will not cause the task referencing +it to be INVALID, follow the task name with pipe (`|`) character. This feature +is particularly useful when a property is used in the task name. + +``` + preload-task = Optional task ${_name} | +``` + +To define an alternate task to load when an optional task does not exist, +list the alternate task name after the pipe (`|`) character. This feature +may be chained together as many times as needed. + +``` + subtask = Optional Subtask ${_name} | + Backup Optional Subtask ${_name} Backup | + Default Subtask # Must exist if the above two don't! +``` + +<a id="task_reference"/> +Task Reference +--------- + +Tasks reference can be a simple task name when the defined task is intended to be in +the same file, tasks from other files and refs can also be referenced by syntax explained +below. + +``` + TASK_REFERENCE = [ + [ // TASK_FILE_PATH ] + [ @USERNAME [ TASK_FILE_PATH ] ] | + [ %GROUP_NAME [ TASK_FILE_PATH ] ] | + [ %%GROUP_UUID [ TASK_FILE_PATH ] ] | + [ TASK_FILE_PATH ] + ] '^' TASK_NAME +``` + +To reference a task from root task.config (top level task.config file of a repository) +on the current ref, prefix the task name with `^`. + +Example: + +task/.../<any>.config +``` + ... + preload-task = ^Task in root task config + ... +``` + +task.config +``` + ... + [task "Task in root task config"] + ... +``` + +To provide an absolute reference to a task under the `task` folder, provide the subpath starting +from `task` directory with a leading `/` followed by a `^` and then task name. + +Example: + +task.config +``` + ... + subtask = /foo/bar/baz.config^Absolute Example Task + ... +``` + +task/foo/bar/baz.config +``` + ... + [task "Absolute Example Task"] + ... +``` + +Similarly, to provide reference to tasks which are in a subdirectory of the file containing the +current task avoid the leading `/`. + +Example: + +task/foo/file.config +``` + ... + subtask = bar/baz.config^Relative Example Task + ... +``` + +task/foo/bar/baz.config +``` + ... + [task "Relative Example Task"] + ... +``` + +Relative tasks specified in a root task.config would look for a file path under the task directory. + +Example: + +task.config +``` + ... + subtask = foo/bar.config^Relative from Root Example Task + ... +``` + +task/foo/bar.config +``` + ... + [task "Relative from Root Example Task"] + ... +``` + +To reference a task from a specific user ref (All-Users.git:refs/users/<user>), specify the +username with `@`. + +when referencing from user refs, to get task from top level task.config on a user ref use +`@<username>^<task_name>` and to get any task under the task directory use the relative +path, like: `@<username>/<relative path from task dir>^<task_name>`. It doesn't matter which +project, ref and file one is referencing from while using this syntax. + +Example: +Assumption: Account id of user_a is 1000000 + +All-Users:refs/users/00/1000000:task.config +``` + ... + [task "top level task"] + ... +``` + +All-Users:refs/users/00/1000000:/task/dir/common.config +``` + ... + [task "common task"] + ... +``` + +All-Projects:refs/meta/config:/task.config +``` + ... + preload-task = @user_a_username^top level task + preload-task = @user_a_username/dir/common.config^common task + ... +``` + +To reference a task from root task.config on the All-Projects.git, prefix the task name with `//^` +and to reference a task from task dir on the All-Projects.git, use +`//<relative path from task dir>^<task_name>`. It doesn't matter which project, ref and file one +is referencing from while using this syntax. + +Example: + +All-Projects:refs/meta/config:task.config +``` + ... + [task "root task"] + ... +``` + +All-Projects:refs/meta/config:/task/dir/sample.config + +``` + ... + [task "sample task"] + ... +``` + +All-Users:refs/users/00/1000000:task.config +``` + ... + preload-task = //dir/sample.config^sample task + preload-task = //^root task + ... +``` + +To reference a task from a specific group ref (All-Users.git:refs/groups/<sharded-group-uuid>), +specify the group name with `%` or group uuid with `%%`. + +When referencing from group refs, to get task from top level task.config on a group ref use +`%<group_name>^<task_name>` or `%%<group_uuid>^<task_name>` and to get any task under the +task directory use the relative path, +like: `%<group_name>/<relative path from task dir>^<task_name>` or +`%%<group_uuid>/<relative path from task dir>^<task_name>`. +It doesn't matter which project, ref and file one is referencing from while using this syntax. + +Example: +Assumption: Group uuid of group_a is 720269095421a08a24889e29d092df1839a7a706 + +All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:task.config +``` + ... + [task "top level task"] + ... +``` + +All-Users:refs/groups/72/720269095421a08a24889e29d092df1839a7a706:/task/dir/common.config +``` + ... + [task "common task"] + ... +``` + +All-Projects:refs/meta/config:/task.config +``` + ... + preload-task = %group_a^top level task + preload-task = %%720269095421a08a24889e29d092df1839a7a706/dir/common.config^common task + ... +```
diff --git a/src/main/resources/Documentation/test/paths.md b/src/main/resources/Documentation/test/paths.md new file mode 100644 index 0000000..8f43cb0 --- /dev/null +++ b/src/main/resources/Documentation/test/paths.md
@@ -0,0 +1,208 @@ +`task.config` file in project `All-Projects` on ref `refs/meta/config`. + +``` +[root "Root Task PATHS"] + subtask = subtask pass + +[task "subtask pass"] + applicable = is:open + pass = is:open + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Task PATHS", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "Root Task PATHS", + "project" : "All-Projects", + "type" : "root" + }, + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "subtask pass", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "subtask pass", + "project" : "All-Projects", + "type" : "task" + }, + "status" : "PASS" + } + ] +} + +[root "Root other FILE"] + applicable = is:open + subtasks-file = common.config + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root other FILE", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "Root other FILE", + "project" : "All-Projects", + "type" : "root" + }, + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config PASS", + "path" : { + "ref" : "refs/meta/config", + "file" : "task/common.config", + "name" : "file task/common.config PASS", + "project" : "All-Projects", + "type" : "task" + }, + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config FAIL", + "path" : { + "ref" : "refs/meta/config", + "file" : "task/common.config", + "name" : "file task/common.config FAIL", + "project" : "All-Projects", + "type" : "task" + }, + "status" : "FAIL" + } + ] +} + +[root "Root tasks-factory"] + subtasks-factory = tasks-factory example + +[tasks-factory "tasks-factory example"] + names-factory = names-factory example list + +[names-factory "names-factory example list"] + type = static + name = my a task + name = my b task + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root tasks-factory", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "Root tasks-factory", + "project" : "All-Projects", + "type" : "root" + }, + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "my a task", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "my a task", + "project" : "All-Projects", + "tasksFactory" : "tasks-factory example", + "type" : "tasks-factory" + }, + "status" : "INVALID" + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "my b task", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "my b task", + "project" : "All-Projects", + "tasksFactory" : "tasks-factory example", + "type" : "tasks-factory" + }, + "status" : "INVALID" + } + ] +} + +[root "Root other PROJECT"] + subtasks-external = user ref + +[external "user ref"] + user = testuser + file = common.config + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root other PROJECT", + "path" : { + "ref" : "refs/meta/config", + "file" : "task.config", + "name" : "Root other PROJECT", + "project" : "All-Projects", + "type" : "root" + }, + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config PASS", + "path" : { + "ref" : "{testuser_user_ref}", + "file" : "task/common.config", + "name" : "file task/common.config PASS", + "project" : "All-Users", + "user" : "testuser", + "type" : "task" + }, + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config FAIL", + "path" : { + "ref" : "{testuser_user_ref}", + "file" : "task/common.config", + "name" : "file task/common.config FAIL", + "project" : "All-Users", + "user" : "testuser", + "type" : "task" + }, + "status" : "FAIL" + } + ] +} +``` +`task.config` file in project `All-Projects` on ref `refs/meta/config`. + +``` +[root "Root Capability Error"] + applicable = is:open + pass = true + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Capability Error", + "path" : { + "error" : "Can't perform operation, need viewTaskPaths capability" + }, + "status" : "PASS" +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/preview.md b/src/main/resources/Documentation/test/preview.md index e53b7d8..df25a6e 100644 --- a/src/main/resources/Documentation/test/preview.md +++ b/src/main/resources/Documentation/test/preview.md
@@ -1,3 +1,4 @@ +``` [root "INVALIDS Preview"] subtasks-file = invalids.config @@ -102,6 +103,22 @@ ] }, { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask Blank", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "name" : "Bad APPLICABLE query", + "status" : "INVALID" + }, + { "applicable" : false, "hasPass" : true, "name" : "NA Bad PASS query", @@ -122,9 +139,13 @@ "status" : "INVALID" # Only Test Suite: invalid }, { + "name" : "UNKNOWN", + "status" : "INVALID" + }, + { "applicable" : true, "hasPass" : false, - "name" : "Looping", + "name" : "task (tasks-factory missing)", "status" : "WAITING", "subTasks" : [ { @@ -134,13 +155,21 @@ ] }, { - "name" : "UNKNOWN", - "status" : "INVALID" + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory static INVALID)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] }, { "applicable" : true, "hasPass" : false, - "name" : "task (tasks-factory missing)", + "name" : "task (tasks-factory change INVALID)", "status" : "WAITING", "subTasks" : [ { @@ -176,6 +205,18 @@ { "applicable" : true, "hasPass" : false, + "name" : "task (names-factory name Blank)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, "name" : "task (names-factory duplicate)", "status" : "WAITING", "subTasks" : [ @@ -226,38 +267,6 @@ "status" : "INVALID" } ] - }, - { - "applicable" : true, - "hasPass" : false, - "name" : "task (tasks-factory changes loop)", - "status" : "WAITING", - "subTasks" : [ - { - "applicable" : true, - "hasPass" : true, - "name" : "_change1_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - }, - { - "applicable" : true, - "hasPass" : true, - "name" : "_change2_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - } - ] } ] } @@ -312,16 +321,28 @@ "status" : "WAITING", "subTasks" : [ { - "applicable" : true, - "hasPass" : true, - "name" : "userfile task/special.config PASS", - "status" : "PASS" + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "userfile task/special.config PASS", # Only Test Suite: secret + "status" : "PASS" # Only Test Suite: secret + "name" : "UNKNOWN", # Only Test Suite: !secret + "status" : "UNKNOWN" # Only Test Suite: !secret }, { - "applicable" : true, - "hasPass" : true, - "name" : "userfile task/special.config FAIL", - "status" : "FAIL" + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "userfile task/special.config FAIL", # Only Test Suite: secret + "status" : "FAIL" # Only Test Suite: secret + "name" : "UNKNOWN", # Only Test Suite: !secret + "status" : "UNKNOWN" # Only Test Suite: !secret + }, + { + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "file task/common.config Preload PASS", # Only Test Suite: secret + "status" : "PASS" # Only Test Suite: secret + "name" : "UNKNOWN", # Only Test Suite: !secret + "status" : "UNKNOWN" # Only Test Suite: !secret } ] } @@ -442,6 +463,22 @@ ] }, { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask Blank", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "name" : "Bad APPLICABLE query", + "status" : "INVALID" + }, + { "applicable" : false, "hasPass" : true, "name" : "NA Bad PASS query", @@ -462,9 +499,13 @@ "status" : "INVALID" # Only Test Suite: invalid }, { + "name" : "UNKNOWN", + "status" : "INVALID" + }, + { "applicable" : true, "hasPass" : false, - "name" : "Looping", + "name" : "task (tasks-factory missing)", "status" : "WAITING", "subTasks" : [ { @@ -474,13 +515,21 @@ ] }, { - "name" : "UNKNOWN", - "status" : "INVALID" + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory static INVALID)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] }, { "applicable" : true, "hasPass" : false, - "name" : "task (tasks-factory missing)", + "name" : "task (tasks-factory change INVALID)", "status" : "WAITING", "subTasks" : [ { @@ -516,6 +565,18 @@ { "applicable" : true, "hasPass" : false, + "name" : "task (names-factory name Blank)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, "name" : "task (names-factory duplicate)", "status" : "WAITING", "subTasks" : [ @@ -566,38 +627,7 @@ "status" : "INVALID" } ] - }, - { - "applicable" : true, - "hasPass" : false, - "name" : "task (tasks-factory changes loop)", - "status" : "WAITING", - "subTasks" : [ - { - "applicable" : true, - "hasPass" : true, - "name" : "_change1_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - }, - { - "applicable" : true, - "hasPass" : true, - "name" : "_change2_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - } - ] } ] } +```
diff --git a/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md new file mode 100644 index 0000000..3d1d7cc --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/new_root_with_original_with_external_secret_ref.md
@@ -0,0 +1,60 @@ +# --task-preview a new root, original root with subtasks-external pointing to secret user ref. + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root with SECRET external"] + applicable = is:open + subtasks-external = SECRET + + [external "SECRET"] + user = {secret_user} + file = secret.config ++ ++[root "Root Preview Simple"] ++ subtask = simple task + ++[task "simple task"] ++ applicable = is:open ++ pass = True +``` + +file: `All-Users.git:{secret_user_ref}:task/secret.config` +``` +[task "SECRET task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root with SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] +} +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Preview Simple", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "simple task", + "status" : "PASS" + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md new file mode 100644 index 0000000..d192050 --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@ +# --task-preview a non-secret user ref with subtasks-external pointing to secret user ref. + +file: `All-Projects.git:refs/meta/config:task.config` +``` +[root "Root for NON-SECRET external Preview with SECRET external"] + applicable = "is:open" + pass = True + subtasks-external = NON-SECRET + +[external "NON-SECRET"] + user = {non_secret_user} + file = sample.config +``` + +file: `All-Users:{non_secret_user_ref}:task/sample.config` +``` + [task "NON-SECRET task"] + applicable = is:open + pass = Fail ++ subtasks-external = SECRET + ++[external "SECRET"] ++ user = {secret_user} ++ file = secret.config +``` + +file: `All-Users.git:{secret_user_ref}:task/secret.config` +``` +[task "SECRET task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root for NON-SECRET external Preview with SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "NON-SECRET task", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md new file mode 100644 index 0000000..a0f11eb --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/non_root_with_subtask_from_root_task.md
@@ -0,0 +1,47 @@ +# --task-preview non-root file with subtask pointing root task + +file: `All-Projects.git:refs/meta/config:task.config` +``` +[root "Points to subFile task with rootFile task preview"] + applicable = is:open + pass = True + subtask = foo/bar/baz.config^Preview pointing to rootFile task + +[task "Task in rootFile"] + applicable = is:open + pass = True +``` + +file: `All-Projects.git:refs/meta/config:task/foo/bar/baz.config` +``` + [task "Preview pointing to rootFile task"] + applicable = is:open + pass = Fail ++ subtask = ^Task in rootFile +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Points to subFile task with rootFile task preview", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Preview pointing to rootFile task", + "status" : "READY", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Task in rootFile", + "status" : "PASS" + } + ] + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md new file mode 100644 index 0000000..c0add08 --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/root_with_external_non-secret_ref_with_external_secret_ref.md
@@ -0,0 +1,60 @@ +# --task-preview root file with subtasks-external pointing to a non-secret user ref with subtasks-external pointing to a secret user ref. + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview NON-SECRET external with SECRET external"] + applicable = "is:open" + pass = True ++ subtasks-external = NON-SECRET with SECRET External + ++[external "NON-SECRET with SECRET External"] ++ user = {non_secret_user} ++ file = secret_external.config +``` + +file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config` +``` +[task "NON-SECRET with SECRET external"] + applicable = is:open + pass = True + subtasks-external = SECRET external + +[external "SECRET external"] + user = {secret_user} + file = secret.config +``` + +file: `All-Users:{secret_user_ref}:task/secret.config` +``` +[task "SECRET task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview NON-SECRET external with SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "NON-SECRET with SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md new file mode 100644 index 0000000..4d81b12 --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/root_with_external_secret_ref.md
@@ -0,0 +1,40 @@ +# --task-preview root file with subtasks-external pointing to secret user ref + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview SECRET external"] + applicable = is:open + pass = True ++ subtasks-external = SECRET external + ++[external "SECRET external"] ++ user = {secret_user} ++ file = secret.config +``` + +file: `All-Users.git:{secret_user_ref}:task/secret.config` +``` +[task "SECRET Task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET Task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md new file mode 100644 index 0000000..504d5a8 --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@ +# --task-preview root file with subtask pointing to a non-secret group ref with subtask pointing to a secret group ref. + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview NON-SECRET group subtask with SECRET group subtask"] + applicable = "is:open" + pass = True ++ subtask = %{non_secret_group_name}/secret_external.config^NON-SECRET with SECRET subtask +``` + +file: `All-Users.git:refs/groups/{sharded_non_secret_group_uuid}:task/secret_external.config` +``` +[task "NON-SECRET with SECRET subtask"] + applicable = is:open + pass = True + subtask = %{secret_group_name}/secret.config^SECRET task +``` + +file: `All-Users:refs/groups/{sharded_secret_group_uuid}:task/secret.config` +``` +[task "SECRET task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview NON-SECRET group subtask with SECRET group subtask", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "NON-SECRET with SECRET subtask", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] + } + ] +} +```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md new file mode 100644 index 0000000..9a1932c --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/subtask_using_group_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@ +# --task-preview root file with subtask pointing to secret group ref + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview SECRET external group"] + applicable = is:open + pass = True ++ subtask = %{secret_group_name}/secret.config^SECRET Task +``` + +file: `All-Users.git:refs/groups/{sharded_secret_group_uuid}:task/secret.config` +``` +[task "SECRET Task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview SECRET external group", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET Task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] +} +```
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md new file mode 100644 index 0000000..fad2f1d --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md
@@ -0,0 +1,52 @@ +# --task-preview root file with subtask pointing to a non-secret user ref with subtask pointing to a secret user ref. + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview NON-SECRET subtask with SECRET subtask"] + applicable = "is:open" + pass = True ++ subtask = @{non_secret_user}/secret_external.config^NON-SECRET with SECRET subtask +``` + +file: `All-Users.git:{non_secret_user_ref}:task/secret_external.config` +``` +[task "NON-SECRET with SECRET subtask"] + applicable = is:open + pass = True + subtask = @{secret_user}/secret.config^SECRET task +``` + +file: `All-Users:{secret_user_ref}:task/secret.config` +``` +[task "SECRET task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview NON-SECRET subtask with SECRET subtask", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "NON-SECRET with SECRET subtask", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md new file mode 100644 index 0000000..d2b3349 --- /dev/null +++ b/src/main/resources/Documentation/test/task-preview/subtask_using_user_syntax/root_with_subtask_secret_ref.md
@@ -0,0 +1,36 @@ +# --task-preview root file with subtask pointing to secret user ref + +file: `All-Projects.git:refs/meta/config:task.config` +``` + [root "Root Preview SECRET external"] + applicable = is:open + pass = True ++ subtask = @{secret_user}/secret.config^SECRET Task +``` + +file: `All-Users.git:{secret_user_ref}:task/secret.config` +``` +[task "SECRET Task"] + applicable = is:open + pass = Fail +``` + +json: +``` +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preview SECRET external", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", # Only Test Suite: non-secret + "status" : "UNKNOWN" # Only Test Suite: non-secret + "applicable" : true, # Only Test Suite: secret + "hasPass" : true, # Only Test Suite: secret + "name" : "SECRET Task", # Only Test Suite: secret + "status" : "READY" # Only Test Suite: secret + } + ] +} +``` \ No newline at end of file
diff --git a/src/main/resources/Documentation/test/task_states.md b/src/main/resources/Documentation/test/task_states.md index ea09541..7a6a8d0 100644 --- a/src/main/resources/Documentation/test/task_states.md +++ b/src/main/resources/Documentation/test/task_states.md
@@ -17,6 +17,7 @@ The config below is expected to be in the `task.config` file in project `All-Projects` on ref `refs/meta/config`. +file: `All-Projects:refs/meta/config:task.config` ``` [root "Root N/A"] applicable = is:closed # Assumes test query is "is:open" @@ -55,12 +56,12 @@ [root "Root PASS"] pass = True -{ - "applicable" : true, - "hasPass" : true, - "name" : "Root PASS", - "status" : "PASS" -} +{ # Test Suite: task_only + "applicable" : true, # Test Suite: task_only + "hasPass" : true, # Test Suite: task_only + "name" : "Root PASS", # Test Suite: task_only + "status" : "PASS" # Test Suite: task_only +} # Test Suite: task_only [root "Root FAIL"] fail = True @@ -72,6 +73,26 @@ "status" : "FAIL" } +[root "Root PASS SR"] + pass = is:true_task + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root PASS SR", + "status" : "PASS" +} + +[root "Root FAIL SR"] + fail = is:true_task + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root FAIL SR", + "status" : "FAIL" +} + [root "Root straight PASS"] applicable = is:open pass = is:open @@ -570,6 +591,12 @@ "hasPass" : true, "name" : "userfile task/special.config FAIL", "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config Preload PASS", + "status" : "PASS" } ] } @@ -597,6 +624,12 @@ "status" : "FAIL" }, { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config Preload PASS", + "status" : "PASS" + }, + { "name" : "UNKNOWN", "status" : "INVALID" } @@ -630,6 +663,12 @@ "status" : "FAIL" }, { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config Preload PASS", + "status" : "PASS" + }, + { "name" : "UNKNOWN", "status" : "INVALID" } @@ -661,6 +700,12 @@ "hasPass" : true, "name" : "userfile task/special.config FAIL", "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config Preload PASS", + "status" : "PASS" } ] } @@ -724,12 +769,14 @@ "subTasks" : [ { "applicable" : true, + "change" : _change1_number, "hasPass" : true, "name" : "_change1_number", "status" : "FAIL" }, { "applicable" : true, + "change" : _change2_number, "hasPass" : true, "name" : "_change2_number", "status" : "FAIL" @@ -765,37 +812,192 @@ "status" : "PASS" } -[root "Root Properties"] - set-root-property = root-value - subtask = Subtask Properties +[root "Root Same Name - Different Tasks-Factory"] + subtasks-factory = parent tasks-factory Same Name - Different Tasks-Factory -[task "Subtask Properties"] - subtask = Subtask Properties Hints - -[task "Subtask Properties Hints"] - set-first-property = first-value - set-second-property = ${first-property} second-extra ${third-property} - set-third-property = third-value +[tasks-factory "parent tasks-factory Same Name - Different Tasks-Factory"] + names-factory = parent names-factory Same Name - Different Tasks-Factory fail = True - fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property}) + subtasks-factory = child tasks-factory Same Name - Different Tasks-Factory + +[names-factory "parent names-factory Same Name - Different Tasks-Factory"] + type = static + name = Same Name + +[tasks-factory "child tasks-factory Same Name - Different Tasks-Factory"] + names-factory = child names-factory Same Name - Different Tasks-Factory + fail = False + +[names-factory "child names-factory Same Name - Different Tasks-Factory"] + type = static + name = Same Name { "applicable" : true, "hasPass" : false, - "name" : "Root Properties", + "name" : "Root Same Name - Different Tasks-Factory", "status" : "WAITING", "subTasks" : [ { "applicable" : true, + "hasPass" : true, + "name" : "Same Name", + "status" : "FAIL", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Same Name", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root Same Name - Different Change"] + subtasks-factory = init tasks-factory Same Name - Different Change + +[tasks-factory "init tasks-factory Same Name - Different Change"] + names-factory = init names-factory Same Name - Different Change + subtask = Same Name - Different Change + +[names-factory "init names-factory Same Name - Different Change"] + type = change + changes = change:_change2_number + +[task "Same Name - Different Change"] + subtasks-factory = tasks-factory Same Name - Different Change + pass = False + ready-hint = continues on to change _change1_number + fail-hint = stops here since we are change _change1_number + fail = change:_change1_number + +[tasks-factory "tasks-factory Same Name - Different Change"] + names-factory = names-factory Same Name - Different Change + subtask = Same Name - Different Change + +[names-factory "names-factory Same Name - Different Change"] + type = change + changes = change:_change1_number NOT change:${_change_number} + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Same Name - Different Change", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change2_number, "hasPass" : false, - "name" : "Subtask Properties", + "name" : "_change2_number", "status" : "WAITING", "subTasks" : [ { "applicable" : true, "hasPass" : true, - "hint" : "root-property(root-value) first-property(first-value) second-property(first-value second-extra third-value)", - "name" : "Subtask Properties Hints", + "name" : "Same Name - Different Change", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : false, + "name" : "_change1_number", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "hint" : "stops here since we are change _change1_number", + "name" : "Same Name - Different Change", + "status" : "FAIL" + } + ] + } + ] + } + ] + } + ] +} + +[root "Root Property References"] + set-first-property = first-value + set-backward-reference = first-[${first-property}] + set-forward-reference = last-[${last-property}] + set-last-property = last-value + fail = True + fail-hint = backward-reference(${backward-reference}) forward-reference(${forward-reference}) + +{ + "applicable" : true, + "hasPass" : true, + "hint" : "backward-reference(first-[first-value]) forward-reference(last-[last-value])", + "name" : "Root Property References", + "status" : "FAIL" +} + +[root "Root Deep Property References"] + set-first-property = first-value + set-direct-reference = first-[${first-property}] + set-deep-reference = deep-{${direct-reference}} + fail = True + fail-hint = deep-reference(${deep-reference}) + +{ + "applicable" : true, + "hasPass" : true, + "hint" : "deep-reference(deep-{first-[first-value]})", + "name" : "Root Deep Property References", + "status" : "FAIL" +} + +[root "Root Properties Referenced Twice"] + set-first-property = first-value + set-referenced-twice = first-[${first-property}] first-[${first-property}] + fail = True + fail-hint = first-[${first-property}] referenced-twice(${referenced-twice}) referenced-twice(${referenced-twice}) + +{ + "applicable" : true, + "hasPass" : true, + "hint" : "first-[first-value] referenced-twice(first-[first-value] first-[first-value]) referenced-twice(first-[first-value] first-[first-value])", + "name" : "Root Properties Referenced Twice", + "status" : "FAIL" +} + +[root "Root Inherited Properties"] + set-root-property = root-value + subtask = Subtask Parent Inherited Properties + +[task "Subtask Parent Inherited Properties"] + set-parent-property = parent-value + subtask = Subtask Inherited Properties + +[task "Subtask Inherited Properties"] + set-my-property = my-value + fail = True + fail-hint = root-property(${root-property}) parent-property(${parent-property}) my-property(${my-property}) + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Inherited Properties", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "Subtask Parent Inherited Properties", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "hint" : "root-property(root-value) parent-property(parent-value) my-property(my-value)", + "name" : "Subtask Inherited Properties", "status" : "FAIL" } ] @@ -803,6 +1005,41 @@ ] } +[root "Root Inherited Distant Properties"] + set-root-property = root-value + set-root-change-property = ${_change_number} + subtask = Subtask Parent Inherited Distant Properties + +[task "Subtask Parent Inherited Distant Properties"] + subtask = Subtask Inherited Distant Properties + +[task "Subtask Inherited Distant Properties"] + fail = True + fail-hint = root-property(${root-property}) root-change-property(${root-change-property}) + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Inherited Distant Properties", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "Subtask Parent Inherited Distant Properties", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "hint" : "root-property(root-value) root-change-property(_change_number)", + "name" : "Subtask Inherited Distant Properties", + "status" : "FAIL" + } + ] + } + ] +} [root "Root Properties Reset By Subtask"] set-root-to-reset-by-subtask = reset-my-root-value @@ -829,6 +1066,46 @@ ] } +[root "Root Inherited Property References"] + set-root-property = root-value + subtask = Subtask Parent Inherited Property References + +[task "Subtask Parent Inherited Property References"] + set-parent-property = parent-value + set-parent-inherited-root-reference = root-property(${root-property}) + subtask = Subtask Inherited Property References + +[task "Subtask Inherited Property References"] + set-inherited-root-reference = root-[${root-property}] + set-inherited-parent-reference = parent-[${parent-property}] + set-inherited-root-deep-reference = parent-inherited-root-reference-[${parent-inherited-root-reference}] + fail = True + fail-hint = inherited-root-reference(${inherited-root-reference}) inherited-parent-reference(${inherited-parent-reference}) inherited-root-deep-reference(${inherited-root-deep-reference}) + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Inherited Property References", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "Subtask Parent Inherited Property References", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "hint" : "inherited-root-reference(root-[root-value]) inherited-parent-reference(parent-[parent-value]) inherited-root-deep-reference(parent-inherited-root-reference-[root-property(root-value)])", + "name" : "Subtask Inherited Property References", + "status" : "FAIL" + } + ] + } + ] +} + [root "Root Properties Exports"] export-root-exported = ${_name} subtask = Subtask Properties Exports @@ -927,6 +1204,99 @@ ] } +[root "Root applicable Property"] + subtask = Subtask applicable Property + subtasks-factory = tasks-factory branch NOT applicable Property + +[tasks-factory "tasks-factory branch NOT applicable Property"] + names-factory = names-factory branch NOT applicable Property + applicable = branch:dev + fail = True + +[names-factory "names-factory branch NOT applicable Property"] + type = static + name = NOT Applicable 1 + name = NOT Applicable 2 + name = NOT Applicable 3 + +[task "Subtask applicable Property"] + applicable = change:${_change_number} + fail = True + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root applicable Property", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask applicable Property", + "status" : "FAIL" + }, # Only Test Suite: all + { # Only Test Suite: all + "applicable" : false, # Only Test Suite: all + "hasPass" : true, # Only Test Suite: all + "name" : "NOT Applicable 1", # Only Test Suite: all + "status" : "FAIL" # Only Test Suite: all + }, # Only Test Suite: all + { # Only Test Suite: all + "applicable" : false, # Only Test Suite: all + "hasPass" : true, # Only Test Suite: all + "name" : "NOT Applicable 2", # Only Test Suite: all + "status" : "FAIL" # Only Test Suite: all + }, # Only Test Suite: all + { # Only Test Suite: all + "applicable" : false, # Only Test Suite: all + "hasPass" : true, # Only Test Suite: all + "name" : "NOT Applicable 3", # Only Test Suite: all + "status" : "FAIL" # Only Test Suite: all + } + ] +} + +[root "Root branch applicable Property"] + subtasks-factory = tasks-factory branch applicable Property + +[tasks-factory "tasks-factory branch applicable Property"] + names-factory = names-factory branch applicable Property + applicable = branch:master + fail = True + +[names-factory "names-factory branch applicable Property"] + type = static + name = Applicable 1 + name = Applicable 2 + name = Applicable 3 + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root branch applicable Property", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Applicable 1", + "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Applicable 2", + "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Applicable 3", + "status" : "FAIL" + } + ] +} + [root "Root Properties tasks-factory STATIC"] subtasks-factory = tasks-factory STATIC Properties @@ -973,7 +1343,7 @@ set-welcome-message = Welcome to the pleasuredome names-factory = names-factory a change fail-hint = ${welcome-message} Name(${_name}) Change Number(${_change_number}) Change Id(${_change_id}) Change Project(${_change_project}) Change Branch(${_change_branch}) Change Status(${_change_status}) Change Topic(${_change_topic}) - fail = True + fail = change:_change1_number [names-factory "names-factory a change"] type = change @@ -987,6 +1357,7 @@ "subTasks" : [ { "applicable" : true, + "change" : _change1_number, "hasPass" : true, "hint" : "Welcome to the pleasuredome Name(_change1_number) Change Number(_change1_number) Change Id(_change1_id) Change Project(_change1_project) Change Branch(_change1_branch) Change Status(_change1_status) Change Topic(_change1_topic)", "name" : "_change1_number", @@ -994,9 +1365,48 @@ }, { "applicable" : true, + "change" : _change2_number, "hasPass" : true, - "hint" : "Welcome to the pleasuredome Name(_change2_number) Change Number(_change2_number) Change Id(_change2_id) Change Project(_change2_project) Change Branch(_change2_branch) Change Status(_change2_status) Change Topic(_change2_topic)", "name" : "_change2_number", + "status" : "PASS" + } + ] +} + +[root "Root tasks-factory _name Property Reference"] + subtasks-factory = Properties tasks-factory _name Property Reference + +[tasks-factory "Properties tasks-factory _name Property Reference"] + set-name-reference = first-property ${_name} + fail-hint = ${name-reference} + fail = true + names-factory = names-factory static list + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root tasks-factory _name Property Reference", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "hint" : "first-property my a task", + "name" : "my a task", + "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "hint" : "first-property my b task", + "name" : "my b task", + "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "hint" : "first-property my c task", + "name" : "my c task", "status" : "FAIL" } ] @@ -1082,12 +1492,14 @@ "subTasks" : [ { "applicable" : true, + "change" : _change_number, "hasPass" : true, "name" : "_change_number", "status" : "FAIL" }, { "applicable" : true, + "change" : _change1_number, "hasPass" : true, "name" : "_change1_number", "status" : "FAIL" @@ -1095,50 +1507,39 @@ ] } -[root "Root Properties Expansion"] - applicable = status:open - subtask = Subtask Property Expansion fail-hint +[root "Root CHANGE constant subtask list CHANGE Properties"] + subtasks-factory = tasks-factory Properties CHANGE names-factory CHANGE -[task "Subtask Property Expansion fail-hint"] - subtasks-factory = tasks-factory Property Expansion fail-hint +[tasks-factory "tasks-factory Properties CHANGE names-factory CHANGE"] + names-factory = Properties names-factory current CHANGE + subtask = Current CHANGE Property -[tasks-factory "tasks-factory Property Expansion fail-hint"] - set-first-property = first-property ${_name} - fail-hint = ${first-property} - fail = true - names-factory = names-factory static list +[task "Current CHANGE Property"] + fail = True + fail-hint = Current Change: ${_change_number} + +[names-factory "Properties names-factory current CHANGE"] + type = change + changes = change:${_change_number} { "applicable" : true, "hasPass" : false, - "name" : "Root Properties Expansion", + "name" : "Root CHANGE constant subtask list CHANGE Properties", "status" : "WAITING", "subTasks" : [ { "applicable" : true, + "change" : _change_number, "hasPass" : false, - "name" : "Subtask Property Expansion fail-hint", + "name" : "_change_number", "status" : "WAITING", "subTasks" : [ { "applicable" : true, "hasPass" : true, - "hint" : "first-property my a task", - "name" : "my a task", - "status" : "FAIL" - }, - { - "applicable" : true, - "hasPass" : true, - "hint" : "first-property my b task", - "name" : "my b task", - "status" : "FAIL" - }, - { - "applicable" : true, - "hasPass" : true, - "hint" : "first-property my c task", - "name" : "my c task", + "hint" : "Current Change: _change_number", + "name" : "Current CHANGE Property", "status" : "FAIL" } ] @@ -1176,6 +1577,158 @@ ] } +[root "Root Properties names-factory Reference"] + subtasks-factory = tasks-factory Properties names-factory Reference + set-predicate = change:_change1_number + +[tasks-factory "tasks-factory Properties names-factory Reference"] + names-factory = Properties names-factory Reference + fail = True + +[names-factory "Properties names-factory Reference"] + type = change + changes = ${predicate} OR change:${_change_number} project:${_change_project} branch:${_change_branch} + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Properties names-factory Reference", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change_number, + "hasPass" : true, + "name" : "_change_number", + "status" : "FAIL" + }, + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL" + } + ] +} + +[root "Root Properties names-factory Deep Reference"] + subtasks-factory = tasks-factory Properties names-factory Deep Reference + set-predicate-reference = ${predicate} + set-predicate = change:_change1_number + +[tasks-factory "tasks-factory Properties names-factory Deep Reference"] + names-factory = Properties names-factory Deep Reference + fail = True + +[names-factory "Properties names-factory Deep Reference"] + type = change + changes = ${predicate-reference} OR change:${_change_number} project:${_change_project} branch:${_change_branch} + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Properties names-factory Deep Reference", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change_number, + "hasPass" : true, + "name" : "_change_number", + "status" : "FAIL" + }, + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL" + } + ] +} + +[root "Root Properties names-factory Reference Internal"] + subtasks-factory = tasks-factory Properties names-factory Reference Internal + set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch} + +[tasks-factory "tasks-factory Properties names-factory Reference Internal"] + names-factory = Properties names-factory Reference Internal + fail = True + +[names-factory "Properties names-factory Reference Internal"] + type = change + changes = change:_change1_number OR ${predicate} + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Properties names-factory Reference Internal", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change_number, + "hasPass" : true, + "name" : "_change_number", + "status" : "FAIL" + }, + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL" + } + ] +} + +[root "Root Properties names-factory Reference Inherited"] + subtask = task Properties names-factory Reference Inherited + set-predicate = change:${_change_number} project:${_change_project} branch:${_change_branch} + +[task "task Properties names-factory Reference Inherited"] + subtasks-factory = tasks-factory Properties names-factory Reference Inherited + +[tasks-factory "tasks-factory Properties names-factory Reference Inherited"] + names-factory = Properties names-factory Reference Inherited + fail = True + +[names-factory "Properties names-factory Reference Inherited"] + type = change + changes = change:_change1_number OR ${predicate} + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Properties names-factory Reference Inherited", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task Properties names-factory Reference Inherited", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change_number, + "hasPass" : true, + "name" : "_change_number", + "status" : "FAIL" + }, + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL" + } + ] + } + ] +} + [root "Root Preload Preload"] subtask = Subtask Preload Preload @@ -1354,10 +1907,17 @@ subtask = Subtask Preload Properties [task "Subtask Preload Properties"] - preload-task = Subtask Properties Hints + preload-task = Subtask Preload Properties Hints set-fourth-property = fourth-value fail-hint = second-property(${second-property}) fourth-property(${fourth-property}) +[task "Subtask Preload Properties Hints"] + set-first-property = first-value + set-second-property = ${first-property} second-extra ${third-property} + set-third-property = third-value + fail = True + fail-hint = root-property(${root-property}) first-property(${first-property}) second-property(${second-property}) + { "applicable" : true, "hasPass" : false, @@ -1374,13 +1934,834 @@ ] } +[root "Root Preload tasks-factory"] + subtasks-factory = tasks-factory Preload tasks-factory + +[tasks-factory "tasks-factory Preload tasks-factory"] + names-factory = names-factory static list + preload-task = Subtask PASS + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Preload tasks-factory", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "my a task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "my b task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "my c task", + "status" : "PASS" + } + ] +} + +[root "Root Looping"] + subtask = Looping + +[task "Looping"] + subtask = Looping + pass = True + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Looping", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Looping", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "Looping", + "status" : "DUPLICATE" + } + ] + } + ] +} + +[root "Root Looping DuplicateKey"] + preload-task = DuplicateKey + +[task "Looping DuplicateKey"] + preload-task = DuplicateKey + pass = True + +[task "DuplicateKey"] + duplicate-key = 1234 + subtask = Looping DuplicateKey + + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Looping DuplicateKey", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "Looping DuplicateKey", + "status" : "DUPLICATE" + } + ] +} + +[root "Root changes loop"] + subtask = task (tasks-factory changes loop) + +[task "task (tasks-factory changes loop)"] + subtasks-factory = tasks-factory change loop + +[tasks-factory "tasks-factory change loop"] + names-factory = names-factory change constant + subtask = task (tasks-factory changes loop) + fail = True + +[names-factory "names-factory change constant"] + changes = change:_change1_number OR change:_change2_number + type = change + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root changes loop", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory changes loop)", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory changes loop)", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change1_number", + "status" : "DUPLICATE" + }, + { + "applicable" : true, + "change" : _change2_number, + "hasPass" : true, + "name" : "_change2_number", + "status" : "FAIL", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory changes loop)", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change1_number", + "status" : "DUPLICATE" + }, + { + "applicable" : true, + "change" : _change2_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change2_number", + "status" : "DUPLICATE" + } + ] + } + ] + } + ] + } + ] + }, + { + "applicable" : true, + "change" : _change2_number, + "hasPass" : true, + "name" : "_change2_number", + "status" : "FAIL", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory changes loop)", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : true, + "name" : "_change1_number", + "status" : "FAIL", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory changes loop)", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "change" : _change1_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change1_number", + "status" : "DUPLICATE" + }, + { + "applicable" : true, + "change" : _change2_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change2_number", + "status" : "DUPLICATE" + } + ] + } + ] + }, + { + "applicable" : true, + "change" : _change2_number, + "hasPass" : false, + "hint" : "Duplicate task is non blocking and empty to break the loop", + "name" : "_change2_number", + "status" : "DUPLICATE" + } + ] + } + ] + } + ] + } + ] +} + +[root "Root Import tasks using absolute syntax"] + applicable = is:open + subtask = /relative.config^Root Import task from subdir using relative syntax + subtask = /dir/common.config^Root Import task from root task.config + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Import tasks using absolute syntax", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "Root Import task from subdir using relative syntax", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Sample relative task in sub dir", + "status" : "PASS" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "Root Import task from root task.config", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root Import relative tasks from root config"] + applicable = is:open + subtask = dir/common.config^Root Import task from root task.config + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Import relative tasks from root config", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : false, + "name" : "Root Import task from root task.config", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root subtasks-external user ref with Absolute and Relative syntaxes"] + subtasks-external = user absolute and relative syntaxes + +[external "user absolute and relative syntaxes"] + user = testuser + file = dir/sample.config + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root subtasks-external user ref with Absolute and Relative syntaxes", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Referencing single task from same user ref", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Relative Task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Relative Task in sub dir", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in user root config file", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root Import user tasks"] + applicable = is:open + subtask = @testuser/foo/bar.config^Absolute Task + subtask = @testuser^task in user root config file + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Import user tasks", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in user root config file", + "status" : "PASS" + } + ] +} + +[root "Root Import group tasks"] + applicable = is:open + subtask = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1 + subtask = %{non_secret_group_name_without_space}^task in group root config file 1 + subtask = %{non_secret_group_name_with_space}/foo/bar.config^Absolute Task 3 + subtask = %{non_secret_group_name_with_space}^task in group root config file 3 + subtask = %%{non_secret_group_uuid}/foo/bar.config^Absolute Task 2 + subtask = %%{non_secret_group_uuid}^task in group root config file 2 + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Import group tasks", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task 1", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in group root config file 1", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task 3", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in group root config file 3", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task 2", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in group root config file 2", + "status" : "PASS" + } + ] +} + +[root "Root Reference tasks from All-Projects"] + applicable = is:open + subtask = //^Subtask PASS + subtask = @testuser/dir/relative.config^Import All-Projects root task + subtask = @testuser/dir/relative.config^Import All-Projects non-root task + subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects root task - groups + subtask = %{non_secret_group_name_without_space}/dir/relative.config^Import All-Projects non-root task - groups + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Reference tasks from All-Projects", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "Import All-Projects root task", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "Import All-Projects non-root task", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Sample relative task in sub dir", + "status" : "PASS" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "Import All-Projects root task - groups", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, + "name" : "Import All-Projects non-root task - groups", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Sample relative task in sub dir", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root Preload from all-projects sub-dir which has preload-task in same file"] + preload-task = //dir/common.config^Sample task in sub dir with preload-task from same file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has preload-task in same file", + "status" : "PASS" +} + +[root "Root Preload from all-projects sub-dir which has preload-task in different file"] + preload-task = //dir/common.config^Sample task in sub dir with preload-task from different file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has preload-task in different file", + "status" : "PASS" +} + +[root "Root Preload from all-projects sub-dir"] + preload-task = //dir/common.config^Sample relative task in sub dir + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir", + "status" : "PASS" +} + +[root "Root Preload from all-projects sub-dir which has subtask in same file"] + preload-task = //dir/common.config^Sample relative task in sub dir with subtask from same file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has subtask in same file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Sample relative task in sub dir", + "status" : "PASS" + } + ] +} + +[root "Root Preload from all-projects sub-dir which has subtask in different file"] + preload-task = //dir/common.config^Sample relative task in sub dir with subtask from different file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has subtask in different file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Passing task", + "status" : "PASS" + } + ] +} + +[root "Root Preload from all-projects sub-dir which has subtasks-factory in same file"] + preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-factory from same file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has subtasks-factory in same file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "my a task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "my b task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "my c task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "my d task", + "status" : "PASS" + } + ] +} + +[root "Root Preload from all-projects sub-dir which has subtasks-external in same file"] + preload-task = //dir/common.config^Sample relative task in sub dir with subtasks-external from same file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from all-projects sub-dir which has subtasks-external in same file", + "status" : "WAITING", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "userfile task/special.config PASS", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "userfile task/special.config FAIL", + "status" : "FAIL" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "file task/common.config Preload PASS", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Referencing single task from same user ref", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Relative Task", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Relative Task in sub dir", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "task in user root config file", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task", + "status" : "PASS" + } + ] + } + ] +} + +[root "Root Preload from group ref which has subtasks-file"] + preload-task = %{non_secret_group_name_without_space}^Sample task with subtasks-file + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from group ref which has subtasks-file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task 1", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task 2", + "status" : "PASS" + }, + { + "applicable" : true, + "hasPass" : true, + "name" : "Absolute Task", + "status" : "PASS" + } + ] +} + +[root "Root Preload from user ref"] + preload-task = @testuser/dir/relative.config^Relative Task + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from user ref", + "status" : "PASS" +} + +[root "Root Preload from user ref which has subtask in same file"] + preload-task = @testuser/dir/relative.config^Relative Task with subtask + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from user ref which has subtask in same file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Passing task", + "status" : "PASS" + } + ] +} + +[root "Root Preload from user ref which has subtask in different file"] + preload-task = @testuser/dir/relative.config^Import All-Projects root task + +{ + "applicable" : true, + "hasPass" : false, + "name" : "Root Preload from user ref which has subtask in different file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] +} + +[root "Root Preload from group ref"] + preload-task = %{non_secret_group_name_without_space}^task in group root config file 1 + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from group ref", + "status" : "PASS" +} + +[root "Root Preload from group ref which has subtask in same file"] + preload-task = %{non_secret_group_name_without_space}^task in group root with subtask + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from group ref which has subtask in same file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Passing task", + "status" : "PASS" + } + ] +} + +[root "Root Preload from group ref which has subtask in different file"] + preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from all-projects + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from group ref which has subtask in different file", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask PASS", + "status" : "PASS" + } + ] +} + +[root "Root Preload from group ref which has subtask in different group ref"] + preload-task = %{non_secret_group_name_without_space}^task in group root with subtask from another group + +{ + "applicable" : true, + "hasPass" : true, + "name" : "Root Preload from group ref which has subtask in different group ref", + "status" : "PASS", + "subTasks" : [ + { + "applicable" : true, + "hasPass" : true, + "name" : "task in group root config file 3", + "status" : "PASS" + } + ] +} + [root "Root INVALID Preload"] preload-task = missing -{ - "name" : "UNKNOWN", - "status" : "INVALID" -} +{ # Test Suite: task_only + "name" : "UNKNOWN", # Test Suite: task_only + "status" : "INVALID" # Test Suite: task_only +} # Test Suite: task_only [root "INVALIDS"] subtasks-file = invalids.config @@ -1486,6 +2867,23 @@ ] }, { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask Blank", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "name" : "Bad APPLICABLE query", # Only Test Suite: visible + "name" : "UNKNOWN", # Only Test Suite: !visible + "status" : "INVALID" + }, + { "applicable" : false, "hasPass" : true, "name" : "NA Bad PASS query", @@ -1506,9 +2904,13 @@ "status" : "INVALID" # Only Test Suite: !all }, { + "name" : "UNKNOWN", + "status" : "INVALID" + }, + { "applicable" : true, "hasPass" : false, - "name" : "Looping", + "name" : "task (tasks-factory missing)", "status" : "WAITING", "subTasks" : [ { @@ -1518,13 +2920,21 @@ ] }, { - "name" : "UNKNOWN", - "status" : "INVALID" + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory static INVALID)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] }, { "applicable" : true, "hasPass" : false, - "name" : "task (tasks-factory missing)", + "name" : "task (tasks-factory change INVALID)", "status" : "WAITING", "subTasks" : [ { @@ -1560,6 +2970,18 @@ { "applicable" : true, "hasPass" : false, + "name" : "task (names-factory name Blank)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, "name" : "task (names-factory duplicate)", "status" : "WAITING", "subTasks" : [ @@ -1610,38 +3032,6 @@ "status" : "INVALID" } ] - }, - { - "applicable" : true, - "hasPass" : false, - "name" : "task (tasks-factory changes loop)", - "status" : "WAITING", - "subTasks" : [ - { - "applicable" : true, - "hasPass" : true, - "name" : "_change1_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - }, - { - "applicable" : true, - "hasPass" : true, - "name" : "_change2_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - } - ] } ] } @@ -1773,6 +3163,23 @@ ] }, { + "applicable" : true, + "hasPass" : true, + "name" : "Subtask Blank", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "name" : "Bad APPLICABLE query", # Only Test Suite: visible + "name" : "UNKNOWN", # Only Test Suite: !visible + "status" : "INVALID" + }, + { "applicable" : false, "hasPass" : true, "name" : "NA Bad PASS query", @@ -1793,9 +3200,13 @@ "status" : "INVALID" # Only Test Suite: !all }, { + "name" : "UNKNOWN", + "status" : "INVALID" + }, + { "applicable" : true, "hasPass" : false, - "name" : "Looping", + "name" : "task (tasks-factory missing)", "status" : "WAITING", "subTasks" : [ { @@ -1805,13 +3216,21 @@ ] }, { - "name" : "UNKNOWN", - "status" : "INVALID" + "applicable" : true, + "hasPass" : false, + "name" : "task (tasks-factory static INVALID)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] }, { "applicable" : true, "hasPass" : false, - "name" : "task (tasks-factory missing)", + "name" : "task (tasks-factory change INVALID)", "status" : "WAITING", "subTasks" : [ { @@ -1847,6 +3266,18 @@ { "applicable" : true, "hasPass" : false, + "name" : "task (names-factory name Blank)", + "status" : "WAITING", + "subTasks" : [ + { + "name" : "UNKNOWN", + "status" : "INVALID" + } + ] + }, + { + "applicable" : true, + "hasPass" : false, "name" : "task (names-factory duplicate)", "status" : "WAITING", "subTasks" : [ @@ -1897,46 +3328,13 @@ "status" : "INVALID" } ] - }, - { - "applicable" : true, - "hasPass" : false, - "name" : "task (tasks-factory changes loop)", - "status" : "WAITING", - "subTasks" : [ - { - "applicable" : true, - "hasPass" : true, - "name" : "_change1_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - }, - { - "applicable" : true, - "hasPass" : true, - "name" : "_change2_number", - "status" : "FAIL", - "subTasks" : [ - { - "name" : "UNKNOWN", - "status" : "INVALID" - } - ] - } - ] } ] } ``` -`task/common.config` file in project `All-Projects` on ref `refs/meta/config`. - +file: `All-Projects:refs/meta/config:task/common.config` ``` [task "file task/common.config PASS"] applicable = is:open @@ -1947,8 +3345,84 @@ fail = is:open ``` -`task/invalids.config` file in project `All-Projects` on ref `refs/meta/config`. +file: `All-Projects:refs/meta/config:task/relative.config` +``` +[task "Root Import task from subdir using relative syntax"] + subtask = dir/common.config^Sample relative task in sub dir +[task "Passing task"] + applicable = is:open + pass = True +``` + +file: `All-Projects:refs/meta/config:task/dir/common.config` +``` +[task "Sample relative task in sub dir"] + applicable = is:open + pass = is:open + +[task "Sample relative task in sub dir with subtask from same file"] + applicable = is:open + pass = is:open + subtask = Sample relative task in sub dir + +[task "Sample relative task in sub dir with subtasks-factory from same file"] + applicable = is:open + pass = is:open + set-my-factory-prop = simple static tasks-factory 2 + subtasks-factory = simple static tasks-factory 1 + subtasks-factory = ${my-factory-prop} + +[tasks-factory "simple static tasks-factory 1"] + names-factory = names-factory static list 1 + pass = True + +[names-factory "names-factory static list 1"] + type = static + name = my a task + name = my b task + name = my c task + +[tasks-factory "simple static tasks-factory 2"] + names-factory = names-factory static list 2 + pass = True + +[names-factory "names-factory static list 2"] + type = static + name = my d task + +[task "Sample relative task in sub dir with subtasks-external from same file"] + applicable = is:open + pass = is:open + set-my-external-prop = user sample config + subtasks-external = user special tasks + subtasks-external = ${my-external-prop} + +[external "user special tasks"] + user = testuser + file = special.config + +[external "user sample config"] + user = testuser + file = dir/sample.config + +[task "Sample relative task in sub dir with subtask from different file"] + applicable = is:open + pass = is:open + subtask = //relative.config^Passing task + +[task "Root Import task from root task.config"] + applicable = is:open + subtask = ^Subtask PASS + +[task "Sample task in sub dir with preload-task from same file"] + preload-task = Sample relative task in sub dir + +[task "Sample task in sub dir with preload-task from different file"] + preload-task = %{non_secret_group_name_without_space}/foo/bar.config^Absolute Task 1 +``` + +file: `All-Projects:refs/meta/config:task/invalids.config` ``` [task "No PASS criteria"] fail-hint = Invalid without Pass criteria and without subtasks @@ -1977,6 +3451,14 @@ [task "Subtask Optional"] subtask = MISSING | MISSING +[task "Subtask Blank"] + pass = True + subtask = + +[task "Bad APPLICABLE query"] + applicable = bad:query + fail = True + [task "NA Bad PASS query"] applicable = NOT is:open # Assumes test query is "is:open" fail = True @@ -1992,23 +3474,30 @@ fail = True in-progress = has:bad -[task "Looping"] - subtask = Looping - [task "Looping Properties"] set-A = ${B} set-B = ${A} + fail-hint = ${A} fail = True [task "task (tasks-factory missing)"] subtasks-factory = missing +[task "task (tasks-factory static INVALID)"] + subtasks-factory = tasks-factory (preload-task missing) + +[task "task (tasks-factory change INVALID)"] + subtasks-factory = tasks-factory change (preload-task missing) + [task "task (names-factory type missing)"] subtasks-factory = tasks-factory (names-factory type missing) [task "task (names-factory type INVALID)"] subtasks-factory = tasks-factory (names-factory type INVALID) +[task "task (names-factory name Blank)"] + subtasks-factory = tasks-factory (names-factory name Blank) + [task "task (names-factory duplicate)"] subtasks-factory = tasks-factory (names-factory duplicate) @@ -2021,16 +3510,27 @@ [task "task (names-factory changes invalid)"] subtasks-factory = tasks-factory change (names-factory changes invalid) -[task "task (tasks-factory changes loop)"] - subtasks-factory = tasks-factory change loop - [tasks-factory "tasks-factory (names-factory type missing)"] names-factory = names-factory (type missing) fail = True +[tasks-factory "tasks-factory (preload-task missing)"] + names-factory = names-factory static + fail = True + preload-task = missing + +[tasks-factory "tasks-factory change (preload-task missing)"] + names-factory = names-factory change list + fail = True + preload-task = missing + [tasks-factory "tasks-factory (names-factory type INVALID)"] names-factory = name-factory (type INVALID) +[tasks-factory "tasks-factory (names-factory name Blank)"] + names-factory = names-factory (name Blank) + fail = True + [tasks-factory "tasks-factory (names-factory duplicate)"] names-factory = names-factory duplicate fail = True @@ -2047,10 +3547,13 @@ names-factory = names-factory change list (changes invalid) fail = True -[tasks-factory "tasks-factory change loop"] - names-factory = names-factory change constant - subtask = task (tasks-factory changes loop) - fail = True +[names-factory "names-factory static"] + name = task A + type = static + +[names-factory "names-factory change list"] + changes = change:_change1_number OR change:_change2_number + type = change [names-factory "names-factory (type missing)"] name = no type test @@ -2062,6 +3565,10 @@ name = invalid type test type = invalid +[names-factory "names-factory (name Blank)"] + name = + type = static + [names-factory "names-factory duplicate"] name = duplicate name = duplicate @@ -2071,17 +3578,12 @@ type = change [names-factory "names-factory change list (changes invalid)"] - change = change:invalidChange - type = change - -[names-factory "names-factory change constant"] - changes = change:_change1_number OR change:_change2_number + changes = change:invalidChange type = change ``` -`task/special.config` file in project `All-Users` on ref `refs/users/self`. - +file: `All-Users:refs/meta/config:task/special.config` ``` [task "userfile task/special.config PASS"] applicable = is:open @@ -2090,4 +3592,154 @@ [task "userfile task/special.config FAIL"] applicable = is:open fail = is:open + +[task "file task/common.config Preload PASS"] + preload-task = userfile task/special.config PASS +``` + +file: `All-Users:refs/users/self:task/common.config` +``` +[task "file task/common.config PASS"] + applicable = is:open + pass = is:open + +[task "file task/common.config FAIL"] + applicable = is:open + fail = is:open +``` + +file: `All-Users:refs/users/self:task.config` +``` +[task "task in user root config file"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/users/self:task/dir/sample.config` +``` +[task "Referencing single task from same user ref"] + applicable = is:open + pass = is:open + subtask = relative.config^Relative Task + subtask = sub_dir/relative.config^Relative Task in sub dir + subtask = ^task in user root config file + subtask = /foo/bar.config^Absolute Task +``` + +file: `All-Users:refs/users/self:task/dir/relative.config` +``` +[task "Relative Task"] + applicable = is:open + pass = is:open + +[task "Relative Task with subtask"] + applicable = is:open + pass = is:open + subtask = Passing task + +[task "Passing task"] + applicable = is:open + pass = True + +[task "Import All-Projects root task"] + applicable = is:open + subtask = //^Subtask PASS + +[task "Import All-Projects non-root task"] + applicable = is:open + subtask = //dir/common.config^Sample relative task in sub dir +``` + +file: `All-Users:refs/users/self:task/dir/sub_dir/relative.config` +``` +[task "Relative Task in sub dir"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/users/self:task/foo/bar.config` +``` +[task "Absolute Task"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/dir/relative.config` +``` +[task "Import All-Projects root task - groups"] + applicable = is:open + subtask = //^Subtask PASS + +[task "Import All-Projects non-root task - groups"] + applicable = is:open + subtask = //dir/common.config^Sample relative task in sub dir +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo/bar.config` +``` +[task "Absolute Task 1"] + applicable = is:open + pass = is:open + +[task "Absolute Task 2"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task/foo.config` +``` +[task "Absolute Task"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_without_space}:task.config` +``` +[task "task in group root config file 1"] + applicable = is:open + pass = is:open + +[task "task in group root with subtask"] + applicable = is:open + pass = is:open + subtask = Passing task + +[task "Passing task"] + applicable = is:open + pass = True + +[task "task in group root with subtask from all-projects"] + applicable = is:open + pass = is:open + subtask = //^Subtask PASS + +[task "task in group root with subtask from another group"] + applicable = is:open + pass = is:open + subtask = %{non_secret_group_name_with_space}^task in group root config file 3 + +[task "task in group root config file 2"] + applicable = is:open + pass = is:open + +[task "Sample task with subtasks-file"] + applicable = is:open + pass = is:open + set-my-prop = foo.config + subtasks-file = foo/bar.config + subtasks-file = ${my-prop} +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task/foo/bar.config` +``` +[task "Absolute Task 3"] + applicable = is:open + pass = is:open +``` + +file: `All-Users:refs/groups/{sharded_non_secret_group_uuid_with_space}:task.config` +``` +[task "task in group root config file 3"] + applicable = is:open + pass = is:open ```
diff --git a/src/test/java/com/google/gerrit/common/BooleanTableTest.java b/src/test/java/com/google/gerrit/common/BooleanTableTest.java new file mode 100644 index 0000000..746a0e9 --- /dev/null +++ b/src/test/java/com/google/gerrit/common/BooleanTableTest.java
@@ -0,0 +1,81 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.gerrit.common; + +import junit.framework.TestCase; + +public class BooleanTableTest extends TestCase { + + public void testNulls() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + assertNull(cbt.get("r1", "c1")); + assertNull(cbt.get("r0", "c0")); + + cbt.put("r1", "c0", true); + assertNull(cbt.get("r1", "c1")); + assertNull(cbt.get("r0", "c0")); + + cbt.put("r0", "c1", true); + assertNull(cbt.get("r1", "c1")); + assertNull(cbt.get("r0", "c0")); + } + + public void testRowColumn() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + cbt.put("r1", "c1", true); + cbt.put("r2", "c2", false); + assertTrue(cbt.get("r1", "c1")); + assertNull(cbt.get("r1", "c2")); + assertNull(cbt.get("r2", "c1")); + assertFalse(cbt.get("r2", "c2")); + } + + public void testRowColumnOverride() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + cbt.put("r1", "c1", true); + assertTrue(cbt.get("r1", "c1")); + + cbt.put("r1", "c1", false); + assertFalse(cbt.get("r1", "c1")); + } + + public void testRepeatedColumns() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + cbt.put("r1", "c1", true); + cbt.put("r2", "c1", false); + assertTrue(cbt.get("r1", "c1")); + assertFalse(cbt.get("r2", "c1")); + } + + public void testRepeatedRows() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + cbt.put("r1", "c1", true); + cbt.put("r1", "c2", false); + assertTrue(cbt.get("r1", "c1")); + assertFalse(cbt.get("r1", "c2")); + } + + public void testRepeatedRowsAndColumns() { + BooleanTable<String, String> cbt = new BooleanTable<>(); + cbt.put("r1", "c1", true); + cbt.put("r2", "c1", false); + cbt.put("r1", "c2", true); + cbt.put("r2", "c2", false); + assertTrue(cbt.get("r1", "c1")); + assertFalse(cbt.get("r2", "c1")); + assertTrue(cbt.get("r1", "c2")); + assertFalse(cbt.get("r2", "c2")); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java new file mode 100644 index 0000000..8ea0009 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskExpressionTest.java
@@ -0,0 +1,260 @@ +// Copyright (C) 2021 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.Project; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import java.util.Iterator; +import java.util.NoSuchElementException; +import junit.framework.TestCase; +import org.mockito.Mockito; + +/* + * <ul> + * <li><code> "simple" -> ("simple") required</code> + * <li><code> "world | peace" -> ("world", "peace") required</code> + * <li><code> "shadenfreud |" -> ("shadenfreud") optional</code> + * <li><code> "foo | bar |" -> ("foo", "bar") optional</code> + * <li><code> "/foo^bar | baz |" -> ("task/foo^bar", "baz") optional</code> + * <li><code> "foo^bar | baz |" -> ("cur_dir/foo^bar", "baz") optional</code> + * <li><code> "^bar | baz |" -> ("task.config^bar", "baz") optional</code> + * </ul> + */ +public class TaskExpressionTest extends TestCase { + public static String SIMPLE = "simple"; + public static String WORLD = "world"; + public static String PEACE = "peace"; + public static FileKey file = createFileKey("foo", "bar", "baz"); + + public static TaskKey SIMPLE_TASK = TaskKey.create(file, SIMPLE); + public static TaskKey WORLD_TASK = TaskKey.create(file, WORLD); + public static TaskKey PEACE_TASK = TaskKey.create(file, PEACE); + + public static String SAMPLE = "sample"; + public static String TASK_CFG = "task.config"; + public static String SIMPLE_CFG = "task/simple.config"; + public static String PEACE_CFG = "task/peace.config"; + public static String WORLD_PEACE_CFG = "task/world/peace.config"; + public static String REL_WORLD_PEACE_CFG = "world/peace.config"; + public static String ABS_PEACE_CFG = "/peace.config"; + + public void testBlank() { + TaskExpression exp = getTaskExpression(""); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testRequiredSingleName() { + TaskExpression exp = getTaskExpression(SIMPLE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testOptionalSingleName() { + TaskExpression exp = getTaskExpression(SIMPLE + "|"); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertFalse(it.hasNext()); + } + + public void testRequiredTwoNames() { + TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), WORLD_TASK); + assertTrue(it.hasNext()); + assertEquals(it.next(), PEACE_TASK); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testOptionalTwoNames() { + TaskExpression exp = getTaskExpression(WORLD + "|" + PEACE + "|"); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), WORLD_TASK); + assertTrue(it.hasNext()); + assertEquals(it.next(), PEACE_TASK); + assertFalse(it.hasNext()); + } + + public void testBlankSpaces() { + TaskExpression exp = getTaskExpression(" "); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testRequiredSingleNameLeadingSpaces() { + TaskExpression exp = getTaskExpression(" " + SIMPLE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testRequiredSingleNameTrailingSpaces() { + TaskExpression exp = getTaskExpression(SIMPLE + " "); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testOptionalSingleNameLeadingSpaces() { + TaskExpression exp = getTaskExpression(" " + SIMPLE + "|"); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertFalse(it.hasNext()); + } + + public void testOptionalSingleNameTrailingSpaces() { + TaskExpression exp = getTaskExpression(SIMPLE + "| "); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertFalse(it.hasNext()); + } + + public void testOptionalSingleNameMiddleSpaces() { + TaskExpression exp = getTaskExpression(SIMPLE + " |"); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), SIMPLE_TASK); + assertFalse(it.hasNext()); + } + + public void testRequiredTwoNamesMiddleSpaces() { + TaskExpression exp = getTaskExpression(WORLD + " | " + PEACE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), WORLD_TASK); + assertTrue(it.hasNext()); + assertEquals(it.next(), PEACE_TASK); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testAbsoluteAndRelativeReference() { + TaskExpression exp = + getTaskExpression( + createFileKey(SIMPLE_CFG), + REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE)); + assertTrue(it.hasNext()); + assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE)); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testAbsoluteAndRelativeReferenceFromRoot() { + TaskExpression exp = + getTaskExpression( + createFileKey(TASK_CFG), + REL_WORLD_PEACE_CFG + "^" + SAMPLE + " | " + ABS_PEACE_CFG + "^" + SAMPLE); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), TaskKey.create(createFileKey(WORLD_PEACE_CFG), SAMPLE)); + assertTrue(it.hasNext()); + assertEquals(it.next(), TaskKey.create(createFileKey(PEACE_CFG), SAMPLE)); + assertTrue(it.hasNext()); + assertNoSuchElementException(it); + } + + public void testReferenceFromRoot() { + TaskExpression exp = getTaskExpression(createFileKey(SIMPLE_CFG), " ^" + SAMPLE + " | "); + Iterator<TaskKey> it = exp.iterator(); + assertTrue(it.hasNext()); + assertEquals(it.next(), TaskKey.create(createFileKey(TASK_CFG), SAMPLE)); + assertNoSuchElementException(it); + } + + public void testDifferentKeyOnDifferentFile() { + TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE); + TaskExpression otherExp = getTaskExpression(createFileKey("foo", "bar", "other"), SIMPLE); + assertFalse(exp.key.equals(otherExp.key)); + } + + public void testDifferentKeyOnDifferentBranch() { + TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE); + TaskExpression otherExp = getTaskExpression(createFileKey("foo", "other", "baz"), SIMPLE); + assertFalse(exp.key.equals(otherExp.key)); + } + + public void testDifferentKeyOnDifferentProject() { + TaskExpression exp = getTaskExpression(createFileKey("foo", "bar", "baz"), SIMPLE); + TaskExpression otherExp = getTaskExpression(createFileKey("other", "bar", "baz"), SIMPLE); + assertFalse(exp.key.equals(otherExp.key)); + } + + public void testDifferentKeyOnDifferentExpression() { + TaskExpression exp = getTaskExpression(SIMPLE); + TaskExpression otherExp = getTaskExpression(PEACE); + assertFalse(exp.key.equals(otherExp.key)); + } + + protected static void assertNoSuchElementException(Iterator<TaskKey> it) { + try { + it.next(); + assertTrue(false); + } catch (NoSuchElementException e) { + assertTrue(true); + } + } + + protected TaskExpression getTaskExpression(String expression) { + return getTaskExpression(file, expression); + } + + protected TaskExpression getTaskExpression(FileKey file, String expression) { + AccountCache accountCache = Mockito.mock(AccountCache.class); + GroupCache groupCache = Mockito.mock(GroupCache.class); + TaskReference.Factory factory = Mockito.mock(TaskReference.Factory.class); + Mockito.when(factory.create(Mockito.any(), Mockito.any())) + .thenAnswer( + invocation -> + new TaskReference( + new TaskKey.Builder( + (FileKey) invocation.getArguments()[0], + new AllProjectsName("All-Projects"), + new AllUsersName("All-Users"), + accountCache, + groupCache), + (String) invocation.getArguments()[1])); + return new TaskExpression(factory, file, expression); + } + + protected static FileKey createFileKey(String file) { + return createFileKey("foo", "bar", file); + } + + protected static FileKey createFileKey(String project, String branch, String file) { + return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file); + } +}
diff --git a/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java new file mode 100644 index 0000000..e08240d --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/task/TaskReferenceTest.java
@@ -0,0 +1,299 @@ +// Copyright (C) 2022 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.task; + +import com.google.common.collect.ImmutableSet; +import com.google.gerrit.entities.Account; +import com.google.gerrit.entities.AccountGroup; +import com.google.gerrit.entities.BranchNameKey; +import com.google.gerrit.entities.InternalGroup; +import com.google.gerrit.entities.Project; +import com.google.gerrit.entities.RefNames; +import com.google.gerrit.server.account.AccountCache; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.GroupCache; +import com.google.gerrit.server.config.AllProjectsName; +import com.google.gerrit.server.config.AllUsersName; +import java.sql.Timestamp; +import java.util.NoSuchElementException; +import java.util.Optional; +import junit.framework.TestCase; +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.junit.Test; +import org.mockito.Mockito; + +public class TaskReferenceTest extends TestCase { + private static final String ALL_USERS = "All-Users"; + private static final String ALL_PROJECTS = "All-Projects"; + public static String SIMPLE = "simple"; + public static String ROOT = "task.config"; + public static String COMMON = "task/common.config"; + public static String SUB_COMMON = "task/dir/common.config"; + public static FileKey ROOT_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, ROOT); + public static FileKey COMMON_CFG = createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, COMMON); + public static FileKey SUB_COMMON_CFG = + createFileKey(ALL_PROJECTS, RefNames.REFS_CONFIG, SUB_COMMON); + + public static FileKey SAMPLE_PROJ_CFG = createFileKey("foo", RefNames.REFS_CONFIG, ROOT); + + public static final String TEST_USER = "testuser"; + public static final int TEST_USER_ID = 100000; + public static final Account TEST_USER_ACCOUNT = + Account.builder(Account.id(TEST_USER_ID), new Timestamp(0L)).build(); + public static final String TEST_USER_REF = + "refs/users/" + String.format("%02d", TEST_USER_ID % 100) + "/" + TEST_USER_ID; + public static final FileKey TEST_USER_ROOT_CFG = createFileKey(ALL_USERS, TEST_USER_REF, ROOT); + public static final FileKey TEST_USER_COMMON_CFG = + createFileKey(ALL_USERS, TEST_USER_REF, COMMON); + + public static final AccountGroup.NameKey TEST_GROUP1_NAME = AccountGroup.nameKey("testgroup"); + public static final AccountGroup.NameKey TEST_GROUP2_NAME = AccountGroup.nameKey("test group"); + public static final String TEST_GROUP1_UUID = "526d2bf882635380fbd3b72320464e342fc14533"; + public static final String TEST_GROUP2_UUID = "62aa5663241f31b9483bad66132bd5d416b2bef9"; + public static final InternalGroup TEST_GROUP1 = + buildTestGroup(AccountGroup.id(1), TEST_GROUP1_NAME, AccountGroup.uuid(TEST_GROUP1_UUID)); + public static final InternalGroup TEST_GROUP2 = + buildTestGroup(AccountGroup.id(2), TEST_GROUP2_NAME, AccountGroup.uuid(TEST_GROUP2_UUID)); + public static final String TEST_GROUP1_REF = + "refs/groups/" + TEST_GROUP1_UUID.substring(0, 2) + "/" + TEST_GROUP1_UUID; + public static final String TEST_GROUP2_REF = + "refs/groups/" + TEST_GROUP2_UUID.substring(0, 2) + "/" + TEST_GROUP2_UUID; + public static final FileKey TEST_GROUP1_ROOT_CFG = + createFileKey(ALL_USERS, TEST_GROUP1_REF, ROOT); + public static final FileKey TEST_GROUP1_COMMON_CFG = + createFileKey(ALL_USERS, TEST_GROUP1_REF, COMMON); + public static final FileKey TEST_GROUP2_ROOT_CFG = + createFileKey(ALL_USERS, TEST_GROUP2_REF, ROOT); + public static final FileKey TEST_GROUP2_COMMON_CFG = + createFileKey(ALL_USERS, TEST_GROUP2_REF, COMMON); + + static InternalGroup buildTestGroup( + AccountGroup.Id id, AccountGroup.NameKey nameKey, AccountGroup.UUID uuid) { + return InternalGroup.builder() + .setGroupUUID(uuid) + .setNameKey(nameKey) + .setOwnerGroupUUID(uuid) + .setId(id) + .setVisibleToAll(true) + .setCreatedOn(new Timestamp(0L)) + .setMembers(ImmutableSet.of()) + .setSubgroups(ImmutableSet.of()) + .build(); + } + + @Test + public void testReferencingTaskFromSameFile() throws Exception { + assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, SIMPLE)); + } + + @Test + public void testReferencingTaskFromRootConfig() throws Exception { + String reference = "^" + SIMPLE; + assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingRelativeTaskFromRootConfig() throws Exception { + String reference = " dir/common.config^" + SIMPLE; + assertEquals(createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference)); + } + + @Test + public void testReferencingAbsoluteTaskFromRootConfig() throws Exception { + String reference = " /common.config^" + SIMPLE; + assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(ROOT_CFG, reference)); + } + + @Test + public void testReferencingRelativeDirTask() throws Exception { + String reference = " dir/common.config^" + SIMPLE; + assertEquals( + createTaskKey(SUB_COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference)); + } + + @Test + public void testReferencingRelativeFileTask() throws Exception { + String reference = "common.config^" + SIMPLE; + assertEquals(createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(COMMON_CFG, reference)); + } + + @Test + public void testReferencingAbsoluteTask() throws Exception { + String reference = " /common.config^" + SIMPLE; + assertEquals( + createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingRootAllProjectsTask() throws Exception { + String reference = "//^" + SIMPLE; + assertEquals(createTaskKey(ROOT_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference)); + } + + @Test + public void testReferencingAllProjectsTask() throws Exception { + String reference = "//common.config^" + SIMPLE; + assertEquals( + createTaskKey(COMMON_CFG, SIMPLE), getTaskFromReference(SAMPLE_PROJ_CFG, reference)); + } + + @Test + public void testReferencingRootUserTask() throws Exception { + String reference = "@" + TEST_USER + "^" + SIMPLE; + assertEquals( + createTaskKey(TEST_USER_ROOT_CFG, SIMPLE), getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingUserTaskDir() throws Exception { + String reference = "@" + TEST_USER + "/common.config^" + SIMPLE; + assertEquals( + createTaskKey(TEST_USER_COMMON_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testMultipleUpchars() throws Exception { + String reference = " ^ /common.config^" + SIMPLE; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testEmptyReference() throws Exception { + String empty = ""; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, empty)); + } + + @Test + public void testReferencingRootGroupNameWithoutSpaceTask() throws Exception { + String reference = "%" + TEST_GROUP1_NAME.get() + "^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingRootGroupNameWithSpaceTask() throws Exception { + String reference = "%" + TEST_GROUP2_NAME.get() + "^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP2_ROOT_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingGroupNameWithoutSpaceTaskDir() throws Exception { + String reference = "%" + TEST_GROUP1_NAME.get() + "/common.config^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingUnknownGroupName() throws Exception { + String reference = "%unknown^" + SIMPLE; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingEmptyGroupName() throws Exception { + String reference = "%^" + SIMPLE; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingGroupNameWithSpaceTaskDir() throws Exception { + String reference = "%" + TEST_GROUP2_NAME.get() + "/common.config^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP2_COMMON_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingRootGroupUUIDTask() throws Exception { + String reference = "%%" + TEST_GROUP1_UUID + "^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP1_ROOT_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingGroupUUIDTaskDir() throws Exception { + String reference = "%%" + TEST_GROUP1_UUID + "/common.config^" + SIMPLE; + assertEquals( + createTaskKey(TEST_GROUP1_COMMON_CFG, SIMPLE), + getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingUnknownGroupUUID() throws Exception { + String reference = "%%a8341ade45d83e867c24a2d37f47b410cfdbea6d^" + SIMPLE; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + @Test + public void testReferencingEmptyGroupUUID() throws Exception { + String reference = "%%^" + SIMPLE; + assertNoSuchElementException(() -> getTaskFromReference(SUB_COMMON_CFG, reference)); + } + + protected static TaskKey getTaskFromReference(FileKey file, String expression) { + AccountCache accountCache = Mockito.mock(AccountCache.class); + GroupCache groupCache = Mockito.mock(GroupCache.class); + Mockito.when(accountCache.getByUsername(TEST_USER)) + .thenReturn(Optional.of(AccountState.forAccount(TEST_USER_ACCOUNT))); + Mockito.when(groupCache.get(TEST_GROUP1_NAME)).thenReturn(Optional.of(TEST_GROUP1)); + Mockito.when(groupCache.get(TEST_GROUP2_NAME)).thenReturn(Optional.of(TEST_GROUP2)); + Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP1_UUID))) + .thenReturn(Optional.of(TEST_GROUP1)); + Mockito.when(groupCache.get(AccountGroup.uuid(TEST_GROUP2_UUID))) + .thenReturn(Optional.of(TEST_GROUP2)); + + try { + return new TaskReference( + new TaskKey.Builder( + file, + new AllProjectsName(ALL_PROJECTS), + new AllUsersName(ALL_USERS), + accountCache, + groupCache), + expression) + .getTaskKey(); + } catch (ConfigInvalidException e) { + throw new NoSuchElementException(); + } + } + + protected static TaskKey createTaskKey(FileKey file, String task) { + return TaskKey.create(file, task); + } + + protected static FileKey createFileKey(String project, String branch, String file) { + return FileKey.create(BranchNameKey.create(Project.NameKey.parse(project), branch), file); + } + + protected static void assertNoSuchElementException(Executable f) throws Exception { + try { + f.run(); + assertTrue(false); + } catch (NoSuchElementException e) { + assertTrue(true); + } + } + + @FunctionalInterface + interface Executable { + void run() throws Exception; + } +}
diff --git a/test/check_task_statuses.sh b/test/check_task_statuses.sh index 5b7e161..30e2ece 100755 --- a/test/check_task_statuses.sh +++ b/test/check_task_statuses.sh
@@ -15,277 +15,82 @@ # limitations under the License. # Usage: -# All-Projects.git - must have 'Push' rights on refs/meta/config +# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user +# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user +# 3. All-Projects.git - must have 'accessDatabase' capability for test user +# 4. All-Users.git - must have 'push' rights on refs/users/* for test user +# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users +# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users +# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users +# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users +# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit +# site's git directory (as group ref updates are done directly to git). -# ---- TEST RESULTS ---- -result() { # test [error_message] - local result=$? - if [ $result -eq 0 ] ; then - echo "PASSED - $1 test" - else - echo "*** FAILED *** - $1 test" - RESULT=$result - [ $# -gt 1 ] && echo "$2" - fi -} +create_configs_from_task_states() { + for marker in $(md_file_markers "$DOC_STATES") ; do + local project_name="$(md_file_marker_project "$marker")" + local project_dir="$OUT/$project_name" + local file="$(md_file_marker_file "$marker")" + local ref="$(md_file_marker_ref "$marker")" -# output must match expected to pass -result_out() { # test expected actual - local name=$1 expected=$2 actual=$3 - - [ "$expected" = "$actual" ] - result "$name" "$(diff <(echo "$expected") <(echo "$actual"))" -} - -result_root() { # group root expected_file actual_file - local name="$1 - $(echo "$2" | sed -es'/Root //')" - result_out "$name" "$(get_root "$2" < "$3")" "$(get_root "$2" < "$4")" -} - -# -------- Git Config - -config() { git config -f "$CONFIG" "$@" ; } # [args]... -config_section_keys() { # section > keys ... - # handlers.handler-filter filter.sh -> handler-filter - config -l --name-only |\ - grep "^$1\." | \ - sed -es"/^$1\.//;s/\..*$//" |\ - awk '$0 != prev ; {prev = $0}' -} - -# -------- Pre JSON -------- -# -# pre_json is a "templated json" used in the test docs to express test results. It looks -# like json but has some extra comments to express when a certain output should be used. -# These comments look like: "# Only Test Suite: <suite>" -# - -remove_suite() { # suite < pre_json > json - grep -v "# Only Test Suite: $1" | \ - sed -e's/# Only Test Suite:.*$//; s/ *$//' -} - -remove_not_suite() { remove_suite !"$1" ; } # suite < pre_json > json - -# -------- Test Doc Format -------- -# -# Test Doc Format has intermixed git config task definitions with json roots. This -# makes it easy to define tests close to their outputs. Be aware that all of the -# config will get consolidated into a single file, so non root config will be shared -# amongst all the roots. -# - -# Sample Test Doc for 2 roots: -# -# [root "Root PASS"] -# pass = True -# -# { -# "applicable" : true, -# "hasPass" : true, -# "name" : "Root PASS", -# "status" : "PASS" -# } -# -# [root "Root FAIL"] -# fail = True -# -# { -# <other root> -# } - -# Strip the json from Test Doc formatted text. For the sample above, the output would be: -# -# [root "Root PASS"] -# pass = True -# -# [root "Root FAIL"] -# fail = True -# ... -# -testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config - -# Strip the git config from Test Doc formatted text. For the sample above, the output would be: -# -# { "plugins" : [ -# { "name" : "task", -# "roots" : [ -# { -# "applicable" : true, -# "hasPass" : true, -# "name" : "Root PASS", -# "status" : "PASS" -# }, -# { -# <other root> -# }, -# ... -# } -testdoc_2_pjson() { # < testdoc_format > pjson_task_roots - awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \ - /^\{/ { open=1 }; \ - open && end { print "}," ; end=0 }; \ - /^\}/ { open=0 ; end=1 }; \ - open; \ - END { print "}]}]}" }' -} - -# ---- JSON PARSING ---- - -json_pp() { # < json > json - python -c "import sys, json; \ - print json.dumps(json.loads(sys.stdin.read()), indent=3, \ - separators=(',', ' : '), sort_keys=True)" -} - -json_val_by() { # json index|'key' > value - echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]" -} -json_val_by_key() { json_val_by "$1" "'$2'" ; } # json key > value - -# -------- -gssh() { ssh -x -p "$PORT" "$SERVER" gerrit "$@" ; } # cmd [args]... - -q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command - -gen_change_id() { echo "I$(uuidgen | openssl dgst -sha1 -binary | xxd -p)"; } # > change_id - -commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg - -err() { echo "ERROR: $1" >&2 ; exit 1 ; } - -# Run a test setup command quietly, exit on failure -q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...] - -ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data - -set_change() { # change_json - { CHANGE=("$(json_val_by_key "$1" number)" \ - "$(json_val_by_key "$1" id)" \ - "$(json_val_by_key "$1" project)" \ - "refs/heads/$(json_val_by_key "$1" branch)" \ - "$(json_val_by_key "$1" status)" \ - "$(json_val_by_key "$1" topic)") ; } 2> /dev/null -} - -# change_token change_number change_id project branch status topic < templated_txt > change_txt -replace_change_properties() { - sed -e "s|_change$1_number|$2|g" \ - -e "s|_change$1_id|$3|g" \ - -e "s|_change$1_project|$4|g" \ - -e "s|_change$1_branch|$5|g" \ - -e "s|_change$1_status|$6|g" \ - -e "s|_change$1_topic|$7|g" -} - -replace_default_changes() { - replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}" -} - -replace_user() { # < text_with_testuser > text_with_$USER - sed -e"s/testuser/$USER/" -} - -strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json -strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json - -get_root() { # root < task_plugin_ouptut > root_json - python -c "if True: # NOP to start indent - import sys, json - - roots=json.loads(sys.stdin.read())['plugins'][0]['roots'] - for root in roots: - if 'name' in root.keys() and root['name']=='$1': - print json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True)" -} - -example() { # example_num - echo "$DOC_STATES" | awk '/```/{Q++;E=(Q+1)/2};E=='"$1" | grep -v '```' | replace_user -} - -get_change_num() { # < gerrit_push_response > changenum - local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }') - echo "${url##*\/}" | tr -d -c '[:digit:]' -} - -install_changeid_hook() { # repo - local hook=$(git rev-parse --git-dir)/hooks/commit-msg - scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook" - chmod +x "$hook" -} - -setup_repo() { # repo remote ref [--initial-commit] - local repo=$1 remote=$2 ref=$3 init=$4 - git init "$repo" - ( - cd "$repo" - install_changeid_hook "$repo" - git fetch "$remote" "$ref" - if ! git checkout FETCH_HEAD ; then - if [ "$init" = "--initial-commit" ] ; then - git commit --allow-empty -a -m "Initial Commit" - fi + if [[ "$ref" == refs/groups/* ]] ; then + project_dir="$project_dir-${ref:(-7)}}" + q_setup setup_repo "$project_dir" "$REMOTE_USERS" "$ref" fi - ) -} -update_repo() { # repo remote ref - local repo=$1 remote=$2 ref=$3 - ( - cd "$repo" - git add . - git commit -m 'Testing task plugin' - git push "$remote" HEAD:"$ref" - ) -} + mkdir -p "$(dirname "$project_dir/$file")" + md_marker_content "$DOC_STATES" "$marker" | replace_user \ + | testdoc_2_cfg > "$project_dir/$file" -create_repo_change() { # repo remote ref [change_id] > change_num - local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change" - ( - q cd "$repo" - date > file - q git add . - [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id") - q git commit -m "$msg" - git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num - ) -} - -query_plugins() { # query - gssh query "$@" --format json | head -1 | python -c "import sys, json; \ - plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \ - print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)" -} - -test_tasks() { # name expected_file task_args... - local name=$1 expected=$2 ; shift 2 - local output=$STATUSES.$name out root - - query_plugins "$@" > "$output" - echo "$ROOTS" | while read root ; do - result_root "$name" "$root" "$expected" "$output" + if [[ "$ref" == refs/groups/* ]] ; then + # As support for pushing a change to group refs [1] is not yet in any release, + # push the update behind gerrit's back, directly into git. + # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614 + q_setup update_repo "$project_dir" "$GERRIT_GIT_DIR/All-Users.git" "$ref" + fi done - out=$(diff "$expected" "$output" | head -15) - [ -z "$out" ] - result "$name - Full Test Suite" "$out" } -test_generated() { # name task_args... +test_2generated() { # name task_args... local name=$1 ; shift - test_tasks "$name" "$EXPECTED.$name" "$@" + local out=$(query "$@") + results_suite "$name" "$EXPECTED.$name" "$(echo "$out" | change_plugins 1)" + results_suite "$name 2nd change" "$EXPECTED.$name"2 "$(echo "$out" | change_plugins 2)" } -test_file() { # name task_args... +test_generated() { # name [-l query_user] task_args... local name=$1 ; shift - local expected=$MYDIR/$name output=$STATUSES.$name + query "$@" | change_plugins 1 > "$ACTUAL.$name" + results_suite "$name" "$EXPECTED.$name" "$( < "$ACTUAL.$name")" +} - query "$@" | awk '$0==" \"plugins\" : [",$0==" ],"' > "$output" - out=$(diff "$expected" "$output") - result "$name" "$out" +usage() { # [error_message] + cat <<-EOF +Usage: + "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user> + --untrusted-user <untrusted user> + + --help|-h help text + --server|-s gerrit host + --non-secret-user user who don't have permission + to view other user refs. + --untrusted-user user who doesn't have permission + to view refs/meta/config ref on All-Projects repo + --non-secret-group-without-space non-secret group name without spaces + --non-secret-group-with-space non-secret group name with spaces +EOF + + [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; } + exit 0 } readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS MYDIR=$(dirname -- "$(readlink -f -- "$0")") +MYPROG=$(basename -- "$0") + +source "$MYDIR/lib/lib_helper.sh" +source "$MYDIR/lib/lib_md.sh" + DOCS=$MYDIR/.././src/main/resources/Documentation/test OUT=$MYDIR/../target/tests @@ -295,20 +100,36 @@ USERS=$OUT/All-Users USER_TASKS=$USERS/task -DOC_PREVIEW=$DOCS/preview.md EXPECTED=$OUT/expected -STATUSES=$OUT/statuses +ACTUAL=$OUT/actual ROOT_CFG=$ALL/task.config -COMMON_CFG=$ALL_TASKS/common.config -INVALIDS_CFG=$ALL_TASKS/invalids.config -USER_SPECIAL_CFG=$USER_TASKS/special.config # --- Args ---- -SERVER=$1 -[ -z "$SERVER" ] && { echo "You must specify a server" ; exit ; } + +while (( "$#" )) ; do + case "$1" in + --help|-h) usage ;; + --server|-s) shift ; SERVER=$1 ;; + --non-secret-user) shift ; NON_SECRET_USER=$1 ;; + --untrusted-user) shift ; UNTRUSTED_USER=$1 ;; + --non-secret-group-without-space) shift ; GROUP_NAME_WITHOUT_SPACE=$1 ;; + --non-secret-group-with-space) shift ; GROUP_NAME_WITH_SPACE=$1 ;; + *) usage "invalid argument $1" ;; + esac + shift +done + +[ -z "$SERVER" ] && usage "You must specify --server" +[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user" +[ -z "$UNTRUSTED_USER" ] && usage "You must specify --untrusted-user" +[ -z "$GROUP_NAME_WITHOUT_SPACE" ] && usage "You must specify --non-secret-group-without-space" +[ -z "$GROUP_NAME_WITH_SPACE" ] && usage "You must specify --non-secret-group-with-space" +[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set" + PORT=29418 +HTTP_PORT=8080 PROJECT=test BRANCH=master REMOTE_ALL=ssh://$SERVER:$PORT/All-Projects @@ -320,6 +141,16 @@ CONFIG=$ROOT_CFG +declare -A USER_REFS +USER_REFS["{testuser_user_ref}"]="$(get_user_ref "$USER")" + +declare -A GROUP_EXPANDED_BY_PLACEHOLDER +GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_without_space}"]="$GROUP_NAME_WITHOUT_SPACE" +GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name_with_space}"]="$GROUP_NAME_WITH_SPACE" +GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_uuid}"]="$(get_group_uuid "$GROUP_NAME_WITHOUT_SPACE")" +GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_without_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITHOUT_SPACE")" +GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid_with_space}"]="$(get_sharded_group_uuid "$GROUP_NAME_WITH_SPACE")" + mkdir -p "$OUT" "$ALL_TASKS" "$USER_TASKS" q_setup setup_repo "$ALL" "$REMOTE_ALL" "$REF_ALL" @@ -329,12 +160,12 @@ changes=$(gssh query "status:open limit:2" --format json) set_change "$(echo "$changes" | awk 'NR==1')" ; CHANGE1=("${CHANGE[@]}") set_change "$(echo "$changes" | awk 'NR==2')" ; CHANGE2=("${CHANGE[@]}") -DOC_STATES=$(replace_default_changes < "$DOCS/task_states.md") -example 2 | replace_user | testdoc_2_cfg > "$ROOT_CFG" -example 3 > "$COMMON_CFG" -example 4 > "$INVALIDS_CFG" -example 5 > "$USER_SPECIAL_CFG" +DOC_STATES=$(replace_tokens < "$DOCS/task_states.md") +DOC_PREVIEW=$(replace_tokens < "$DOCS/preview.md") +DOC_PATHS=$(replace_tokens < "$DOCS/paths.md") + +create_configs_from_task_states ROOTS=$(config_section_keys "root") || err "Invalid ROOTS" @@ -342,9 +173,12 @@ q_setup update_repo "$USERS" "$REMOTE_USERS" "$REF_USERS" change3_id=$(gen_change_id) +change4_id=$(gen_change_id) +change4_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change4_id") change3_number=$(create_repo_change "$OUT/$PROJECT" "$REMOTE_TEST" "$BRANCH" "$change3_id") -all_pjson=$(example 2 | testdoc_2_pjson | \ +ex2_pjson=$(example "$DOC_STATES" 2 | testdoc_2_pjson) +all_pjson=$(echo "$ex2_pjson" | \ replace_change_properties \ "" \ "$change3_number" \ @@ -354,36 +188,82 @@ "NEW" \ "") -no_all_json=$(echo "$all_pjson" | remove_suite all) +all2_pjson=$(echo "$ex2_pjson" | \ + replace_change_properties \ + "" \ + "$change4_number" \ + "$change4_id" \ + "$PROJECT" \ + "refs\/heads\/$BRANCH" \ + "NEW" \ + "") -echo "$no_all_json" | strip_non_applicable | \ +no_all_visible_json=$(echo "$all_pjson" | remove_suites "all" "!visible") +no_all_no_visible_json=$(echo "$all_pjson" | remove_suites "all" "visible") +no_all_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "!visible") +no_all_no_visible2_json=$(echo "$all2_pjson" | remove_suites "all" "visible") + +echo "$no_all_visible_json" | strip_non_applicable | \ grep -v "\"applicable\" :" > "$EXPECTED".applicable -echo "$all_pjson" | remove_not_suite all | ensure json_pp > "$EXPECTED".all +echo "$no_all_no_visible_json" | strip_non_applicable | \ + grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility -echo "$no_all_json" | strip_non_invalid > "$EXPECTED".invalid +echo "$no_all_visible2_json" | strip_non_applicable | \ + grep -v "\"applicable\" :" > "$EXPECTED".applicable2 + +echo "$no_all_no_visible2_json" | strip_non_applicable | \ + grep -v "\"applicable\" :" > "$EXPECTED".applicable-visibility2 + +echo "$all_pjson" | remove_suites "!all" "!visible" | ensure json_pp > "$EXPECTED".all + +echo "$no_all_visible_json" | strip_non_invalid > "$EXPECTED".invalid strip_non_invalid < "$EXPECTED".applicable > "$EXPECTED".invalid-applicable -preview_pjson=$(testdoc_2_pjson < "$DOC_PREVIEW" | replace_default_changes) -echo "$preview_pjson" | remove_suite invalid | ensure json_pp > "$EXPECTED".preview -echo "$preview_pjson" | remove_not_suite invalid | strip_non_invalid > "$EXPECTED".preview-invalid +preview_pjson=$(example "$DOC_PREVIEW" 1 | testdoc_2_pjson) +echo "$preview_pjson" | remove_suites "invalid" "secret" | \ + ensure json_pp > "$EXPECTED".preview-non-secret +echo "$preview_pjson" | remove_suites "invalid" "!secret" | \ + ensure json_pp > "$EXPECTED".preview-admin +echo "$preview_pjson" | remove_suites "secret" "!invalid" | \ + strip_non_invalid > "$EXPECTED".preview-invalid -testdoc_2_cfg < "$DOC_PREVIEW" | replace_user > "$ROOT_CFG" +example "$DOC_PREVIEW" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG" cnum=$(create_repo_change "$ALL" "$REMOTE_ALL" "$REF_ALL") PREVIEW_ROOTS=$(config_section_keys "root") RESULT=0 -query="change:$change3_number status:open" -test_generated applicable --task--applicable "$query" +query="(change:$change3_number OR change:$change4_number) status:open" +test_2generated applicable --task--applicable "$query" +test_2generated applicable-visibility -l "$UNTRUSTED_USER" --task--applicable "$query" test_generated all --task--all "$query" test_generated invalid --task--invalid "$query" test_generated invalid-applicable --task--applicable --task--invalid "$query" ROOTS=$PREVIEW_ROOTS -test_generated preview --task--preview "$cnum,1" --task--all "$query" +test_generated preview-admin --task--preview "$cnum,1" --task--all "$query" +test_generated preview-non-secret -l "$NON_SECRET_USER" --task--preview "$cnum,1" --task--all "$query" test_generated preview-invalid --task--preview "$cnum,1" --task--invalid "$query" + +example "$DOC_STATES" 2 | keep_suites "task_only" | testdoc_2_pjson | \ + ensure json_pp > "$EXPECTED".task-roots-filter +test_generated task-roots-filter --task--all --task--only "Root\ PASS" "$query" + +example "$DOC_PATHS" 1 | testdoc_2_cfg | replace_user > "$ROOT_CFG" +q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL" +ROOTS=$(config_section_keys "root") +example "$DOC_PATHS" 1 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths + +test_generated task-paths --task--all --task--include-paths "$query" + +example "$DOC_PATHS" 2 | testdoc_2_cfg > "$ROOT_CFG" +q_setup update_repo "$ALL" "$REMOTE_ALL" "$REF_ALL" +ROOTS=$(config_section_keys "root") +example "$DOC_PATHS" 2 | testdoc_2_pjson | ensure json_pp > "$EXPECTED".task-paths.non-secret +test_generated task-paths.non-secret -l "$NON_SECRET_USER" --task--all --task--include-paths "$query" + exit $RESULT
diff --git a/test/check_task_visibility.sh b/test/check_task_visibility.sh new file mode 100755 index 0000000..1dabec4 --- /dev/null +++ b/test/check_task_visibility.sh
@@ -0,0 +1,248 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2022 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Usage: +# 1. All-Projects.git - must have 'Push' rights on refs/meta/config for test user +# 2. All-Projects.git - must have 'viewTaskPaths' capability for test user +# 3. All-Projects.git - must have 'accessDatabase' capability for test user +# 4. All-Users.git - must have 'push' rights on refs/users/* for test user +# 5. All-Users.git - must have 'push' rights on refs/users/${shardeduserid} for Registered Users +# 6. All-Users.git - must have 'read' rights on refs/users/${shardeduserid} for Registered Users +# 7. All-Users.git - must have 'create' rights on refs/users/${shardeduserid} for Registered Users +# 8. All-Users.git - must deny 'read' rights on refs/* for Anonymous Users +# 9. GERRIT_GIT_DIR environment variable must have the path to gerrit +# site's git directory (as group ref updates are done directly to git). + +readlink -f / &> /dev/null || readlink() { greadlink "$@" ; } # for MacOS +MYDIR=$(dirname -- "$(readlink -f -- "$0")") +MYPROG=$(basename -- "$0") + +source "$MYDIR/lib/lib_helper.sh" +source "$MYDIR/lib/lib_md.sh" + +# Visibility tests cases are described using a markdown file. +# Each file has a list of config files specified by file +# markers. The initial state of task configs is created using +# them. Only one of the config file has an inline diff. Gerrit +# change is created by applying that diff to the specified file +# marker and the expected json is asserted by using that change +# as an input to the '--task-preview' switch. + +# The syntax for inline diff is similar to diff --unified=MAX_INT. +# All lines start with a leading space and if a specific line is +# part of diff, we use diff indicators (+/-) instead of a leading +# space. + +# Example input for all diff functions: +# +# [root "Root Preview SECRET external"] +# applicable = is:open +# pass = True +# - subtask = Subtask APPLICABLE +# + subtasks-external = SECRET external +# +# +[external "SECRET external"] +# + user = {secret_user} +# + file = secret.config + + +# Returns if a config has inline diff or not. +diff_indicators_present() { # file_content + echo "$1" | grep -q "^-\|^+" +} + +# file_content_with_diff_indicators > file_content_with_diff_applied +# out: +#[root "Root Preview SECRET external"] +# applicable = is:open +# pass = True +# subtask = Subtask APPLICABLE +diff_apply() { + sed -e '/^-/d' -e 's/^.//' +} + +# file_content_with_diff_indicators > file_content_with_diff_reverted +# out: +#[root "Root Preview SECRET external"] +# applicable = is:open +# pass = True +# subtasks-external = SECRET external +# +#[external "SECRET external"] +# user = {secret_user} +# file = secret.config +diff_revert() { + sed -e '/^+/d' -e 's/^.//' +} + +config_ensure() { # config_file_path + q git config --list -f "$1" || err "Invalid config file: $1" +} + +get_remote() { # project > remote_url + echo "ssh://$SERVER:$PORT/$(basename "$1")" +} + +# Gets json from the preview doc and creates +# expected json in workspace to assert later. +create_expected_json() { + local json=$(md_marker_content "$TEST_DOC" "json:") + + echo "$json" | remove_suites "non-secret" | \ + testdoc_2_pjson | ensure json_pp > "$EXPECTED_SECRET" + echo "$json" | remove_suites "secret" | \ + testdoc_2_pjson | ensure json_pp > "$EXPECTED_NON_SECRET" +} + +test_preview() { # preview_change_number + query --task--all --task--preview "$1,1" "change:1" \ + | change_plugins 1 > "$ACTUAL_SECRET" + query -l "$NON_SECRET_USER" --task--all --task--preview "$1,1" "change:1" \ + | change_plugins 1 > "$ACTUAL_NON_SECRET" + + ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_SECRET") + results_suite "Visibility Secret Test" "$EXPECTED_SECRET" "$( < "$ACTUAL_SECRET" )" + + ROOTS=$(jq -r '.plugins[].roots | .[].name' < "$EXPECTED_NON_SECRET") + results_suite "Visibility Non-Secret Test" "$EXPECTED_NON_SECRET" "$( < "$ACTUAL_NON_SECRET" )" +} + +init_configs() { + for marker in $(md_file_markers "$TEST_DOC") ; do + local project="$OUT/$(md_file_marker_project "$marker")" + local ref="$(md_file_marker_ref "$marker")" + local file="$(md_file_marker_file "$marker")" + local content="$(md_marker_content "$TEST_DOC" "$marker")" + local tip_content + + q_setup setup_repo "$project" "$(get_remote "$project")" "$ref" + mkdir -p "$(dirname "$project/$file")" + + if diff_indicators_present "$content" ; then + CHANGE_FILE_MARKER=$marker + CHANGE_CONTENT=$(echo "$content" | diff_apply) + tip_content=$(echo "$content" | diff_revert) + else + tip_content=$content + fi + + echo "$tip_content" > "$project/$file" + config_ensure "$project/$file" + if [[ "$ref" == refs/groups/* ]] ; then + # As support for pushing a change to group refs [1] is not yet in any release, + # push the update behind gerrit's back, directly into git. + # [1] https://gerrit-review.googlesource.com/c/gerrit/+/390614 + q_setup update_repo "$project" "$GERRIT_GIT_DIR/All-Users.git" "$ref" + else + q_setup update_repo "$project" "$(get_remote "$project")" "$ref" + fi + done +} + +test_change() { + local project="$OUT/$(md_file_marker_project "$CHANGE_FILE_MARKER")" + local ref="$(md_file_marker_ref "$CHANGE_FILE_MARKER")" + local file="$(md_file_marker_file "$CHANGE_FILE_MARKER")" + q_setup setup_repo "$project" "$(get_remote "$project")" "$ref" + + echo "$CHANGE_CONTENT" > "$project/$file" + config_ensure "$project/$file" + local cnum=$(create_repo_change "$project" "$(get_remote "$project")" "$ref") + + create_expected_json + test_preview "$cnum" +} + +usage() { # [error_message] + cat <<-EOF +Usage: + "$MYPROG" --server <gerrit_host> --non-secret-user <non-secret user> + + --help|-h help text + --server|-s gerrit host + --non-secret-user user who doesn't have permission + to view other user refs. + --non-secret-group non-secret group name + --secret-group secret group name +EOF + + [ -n "$1" ] && { echo "Error: $1" ; exit 1 ; } + exit 0 +} + +while (( "$#" )) ; do + case "$1" in + --help|-h) usage ;; + --server|-s) shift ; SERVER=$1 ;; + --non-secret-user) shift ; NON_SECRET_USER=$1 ;; + --non-secret-group) shift ; NON_SECRET_GROUP_NAME=$1 ;; + --secret-group) shift ; SECRET_GROUP_NAME=$1 ;; + *) usage "invalid argument $1" ;; + esac + shift +done + +[ -z "$SERVER" ] && usage "You must specify --server" +[ -z "$NON_SECRET_USER" ] && usage "You must specify --non-secret-user" +[ -z "$NON_SECRET_GROUP_NAME" ] && usage "You must specify --non-secret-group" +[ -z "$SECRET_GROUP_NAME" ] && usage "You must specify --secret-group" +[ -z "$GERRIT_GIT_DIR" ] && usage "GERRIT_GIT_DIR environment variable not set" + +RESULT=0 +PORT=29418 +HTTP_PORT=8080 +OUT=$MYDIR/../target/preview +EXPECTED_SECRET="$OUT/expected-secret" +EXPECTED_NON_SECRET="$OUT/expected-non-secret" +ACTUAL_SECRET="$OUT/actual-secret" +ACTUAL_NON_SECRET="$OUT/actual-non-secret" +TEST_DOC_DIR="$MYDIR/../src/main/resources/Documentation/test/task-preview/" + +declare -A USERS +declare -A USER_REFS +USERS["{secret_user}"]="$USER" +USER_REFS["{secret_user_ref}"]="$(get_user_ref "$USER")" +USERS["{non_secret_user}"]="$NON_SECRET_USER" +USER_REFS["{non_secret_user_ref}"]="$(get_user_ref "$NON_SECRET_USER")" + +declare -A GROUP_EXPANDED_BY_PLACEHOLDER +GROUP_EXPANDED_BY_PLACEHOLDER["{secret_group_name}"]="$SECRET_GROUP_NAME" +GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_secret_group_uuid}"]="$(get_sharded_group_uuid "$SECRET_GROUP_NAME")" +GROUP_EXPANDED_BY_PLACEHOLDER["{non_secret_group_name}"]="$NON_SECRET_GROUP_NAME" +GROUP_EXPANDED_BY_PLACEHOLDER["{sharded_non_secret_group_uuid}"]="$(get_sharded_group_uuid "$NON_SECRET_GROUP_NAME")" + +mkdir -p "$OUT" +trap 'rm -rf "$OUT"' EXIT + +TESTS=( +"new_root_with_original_with_external_secret_ref.md" +"non-secret_ref_with_external_secret_ref.md" +"root_with_external_non-secret_ref_with_external_secret_ref.md" +"root_with_external_secret_ref.md" +"non_root_with_subtask_from_root_task.md" +"subtask_using_user_syntax/root_with_subtask_secret_ref.md" +"subtask_using_user_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md" +"subtask_using_group_syntax/root_with_subtask_secret_ref.md" +"subtask_using_group_syntax/root_with_subtask_non-secret_ref_with_subtask_secret_ref.md" +) + +for test in "${TESTS[@]}" ; do + TEST_DOC="$(replace_user_refs < "$TEST_DOC_DIR/$test" | replace_users | replace_groups)" + init_configs + test_change +done + +exit $RESULT
diff --git a/test/docker/docker-compose.yaml b/test/docker/docker-compose.yaml index 634cde4..a228122 100755 --- a/test/docker/docker-compose.yaml +++ b/test/docker/docker-compose.yaml
@@ -11,6 +11,7 @@ - gerrit-net volumes: - "gerrit-site-etc:/var/gerrit/etc" + - "gerrit-site-git:/var/gerrit/git" run_tests: build: run_tests @@ -19,10 +20,12 @@ volumes: - "../../:/task:ro" - "gerrit-site-etc:/server-ssh-key:ro" + - "gerrit-site-git:/gerrit-site-git" depends_on: - gerrit-01 environment: - GERRIT_HOST=gerrit-01 + - GERRIT_GIT_DIR=/gerrit-site-git networks: gerrit-net: @@ -30,3 +33,4 @@ volumes: gerrit-site-etc: + gerrit-site-git:
diff --git a/test/docker/gerrit/Dockerfile b/test/docker/gerrit/Dockerfile index 12aa74a..a407854 100755 --- a/test/docker/gerrit/Dockerfile +++ b/test/docker/gerrit/Dockerfile
@@ -1,9 +1,11 @@ -FROM gerritcodereview/gerrit:3.4.0-ubuntu20 +FROM gerritcodereview/gerrit:3.5.6-ubuntu20 ENV GERRIT_SITE /var/gerrit RUN git config -f "$GERRIT_SITE/etc/gerrit.config" auth.type \ DEVELOPMENT_BECOME_ANY_ACCOUNT +RUN touch "$GERRIT_SITE"/.firstTimeRedirect COPY artifacts /tmp/ RUN cp /tmp/task.jar "$GERRIT_SITE/plugins/task.jar" RUN { [ -e /tmp/gerrit.war ] && cp /tmp/gerrit.war "$GERRIT_SITE/bin/gerrit.war" ; } || true +RUN chmod 777 "$GERRIT_SITE/git"
diff --git a/test/docker/run.sh b/test/docker/run.sh index 75b9b3a..5f9b412 100755 --- a/test/docker/run.sh +++ b/test/docker/run.sh
@@ -57,12 +57,12 @@ run_task_plugin_tests() { docker-compose "${COMPOSE_ARGS[@]}" up --detach - docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin run_tests \ + docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin run_tests \ '/task/test/docker/run_tests/start.sh' } retest() { - docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=gerrit_admin \ + docker-compose "${COMPOSE_ARGS[@]}" exec -T --user=admin \ run_tests task/test/docker/run_tests/start.sh retest RESULT=$? cleanup
diff --git a/test/docker/run_tests/Dockerfile b/test/docker/run_tests/Dockerfile index 06691e1..dd5ba8c 100755 --- a/test/docker/run_tests/Dockerfile +++ b/test/docker/run_tests/Dockerfile
@@ -2,12 +2,12 @@ ARG UID=1000 ARG GID=1000 -ENV USER gerrit_admin +ENV USER admin ENV USER_HOME /home/$USER ENV RUN_TESTS_DIR task/test/docker/run_tests ENV WORKSPACE $USER_HOME/workspace -RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd +RUN apk --update add --no-cache openssh bash git python2 shadow util-linux openssl xxd curl jq RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config RUN groupadd -f -g $GID users2 @@ -21,6 +21,6 @@ RUN chmod 400 "$USER_HOME"/.ssh/id_rsa RUN chmod 400 "$USER_HOME"/.ssh/id_rsa.pub RUN git config --global user.name "Gerrit Admin" -RUN git config --global user.email "gerrit_admin@localdomain" +RUN git config --global user.email "admin@example.com" ENTRYPOINT ["tail", "-f", "/dev/null"]
diff --git a/test/docker/run_tests/create-one-time-test-data.sh b/test/docker/run_tests/create-one-time-test-data.sh new file mode 100755 index 0000000..d949bb2 --- /dev/null +++ b/test/docker/run_tests/create-one-time-test-data.sh
@@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +die() { echo -e "\nERROR:" "$@" ; kill $$ ; exit 1 ; } # error_message + +q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command + +gssh() { ssh -x -p "$SSH_PORT" "$GERRIT_HOST" gerrit "$@" ; } # run a gerrit ssh command + +create_test_users_and_group() { + echo "Creating test users and group ..." + gssh create-account "$NON_SECRET_USER" --full-name "$NON_SECRET_USER" \ + --email "$NON_SECRET_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub + + gssh create-account "$UNTRUSTED_USER" --full-name "$UNTRUSTED_USER" \ + --email "$UNTRUSTED_USER"@example.com --ssh-key - < ~/.ssh/id_rsa.pub + + gssh create-group "Visible-All-Projects-Config" --member "$NON_SECRET_USER" + + local secret_user=$USER + gssh create-group "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" \ + --member "$NON_SECRET_USER" --member "$secret_user" + gssh create-group "\"$NON_SECRET_GROUP_NAME_WITH_SPACE\"" \ + --member "$NON_SECRET_USER" --member "$secret_user" + gssh create-group "$SECRET_GROUP_NAME" --member "$secret_user" +} + +setup_all_projects_repo() { + echo "Updating All-Projects repo ..." + + local uuid=$(gssh ls-groups -v | awk '-F\t' '$1 == "Visible-All-Projects-Config" {print $2}') + ( cd "$WORKSPACE" + q git clone ssh://"$GERRIT_HOST":"$SSH_PORT"/All-Projects allProjects + cd allProjects + q git fetch origin refs/meta/config ; q git checkout FETCH_HEAD + echo -e "$uuid\tVisible-All-Projects-Config" >> groups + git config -f "project.config" \ + --add access."refs/meta/config".read "group Visible-All-Projects-Config" + git config -f "project.config" \ + --add capability.viewTaskPaths "group Administrators" +# After migrating to version 3.5, it is no longer feasible to assign read permissions to +# Administrators for another user's ref. To address this, add the 'accessDatabase' capability, +# allowing admins to read the user ref of other users + git config -f "project.config" \ + --add capability.accessDatabase "group Administrators" + q git add . && q git commit -m "project config update" + q git push origin HEAD:refs/meta/config + ) +} + +SSH_PORT=29418 +USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR" +while (( "$#" )) ; do + case "$1" in + --non-secret-user) shift ; NON_SECRET_USER="$1" ;; + --untrusted-user) shift ; UNTRUSTED_USER="$1" ;; + --non-secret-group-without-space) shift ; NON_SECRET_GROUP_NAME_WITHOUT_SPACE="$1" ;; + --non-secret-group-with-space) shift ; NON_SECRET_GROUP_NAME_WITH_SPACE="$1" ;; + --secret-group) shift ; SECRET_GROUP_NAME="$1" ;; + *) die "invalid argument '$1'" ;; + esac + shift +done + +[ -z "$NON_SECRET_USER" ] && die "non-secret-user not set" +[ -z "$UNTRUSTED_USER" ] && die "untrusted-user not set" +[ -z "$NON_SECRET_GROUP_NAME_WITHOUT_SPACE" ] && die "non-secret-group-without-space not set" +[ -z "$NON_SECRET_GROUP_NAME_WITH_SPACE" ] && die "non-secret-group-with-space not set" +[ -z "$SECRET_GROUP_NAME" ] && die "secret-group not set" + +"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh +"$USER_RUN_TESTS_DIR"/update-all-users-project.sh +create_test_users_and_group +setup_all_projects_repo
diff --git a/test/docker/run_tests/start.sh b/test/docker/run_tests/start.sh index ac185d8..4a73f24 100755 --- a/test/docker/run_tests/start.sh +++ b/test/docker/run_tests/start.sh
@@ -1,23 +1,55 @@ #!/usr/bin/env bash +die() { echo "ERROR: $1" >&2 ; exit 1 ; } # errormsg + +is_plugin_loaded() { # plugin_name + ssh -p 29418 "$GERRIT_HOST" gerrit plugin ls | awk '{print $1}' | grep -q "^$1\$" +} + USER_RUN_TESTS_DIR="$USER_HOME"/"$RUN_TESTS_DIR" -cp -r /task "$USER_HOME"/ +mkdir "$USER_HOME"/task && cp -r /task/{src,test} "$USER_HOME"/task if [ "$1" = "retest" ] ; then cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST" exit $? fi -./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 -t 60 -- echo "gerrit is up" +./"$USER_RUN_TESTS_DIR"/wait-for-it.sh "$GERRIT_HOST":29418 \ + -t -60 || die "Failed to start gerrit" +echo "gerrit is up" -echo "Creating a default user account ..." +echo "Update admin account ..." cat "$USER_HOME"/.ssh/id_rsa.pub | ssh -p 29418 -i /server-ssh-key/ssh_host_rsa_key \ - "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit create-account \ - --ssh-key - --email "gerrit_admin@localdomain" --group "Administrators" "gerrit_admin" + "Gerrit Code Review@$GERRIT_HOST" suexec --as "admin@example.com" -- gerrit set-account \ + admin --add-ssh-key - -./"$USER_RUN_TESTS_DIR"/create-test-project-and-changes.sh -./"$USER_RUN_TESTS_DIR"/update-all-users-project.sh +PASSWORD=$(uuidgen) +echo "machine $GERRIT_HOST login $USER password $PASSWORD" > "$USER_HOME"/.netrc +ssh -p 29418 "$GERRIT_HOST" gerrit set-account --http-password "$PASSWORD" "$USER" + +is_plugin_loaded "task" || die "Task plugin is not installed" + +NON_SECRET_USER="non_secret_user" +UNTRUSTED_USER="untrusted_user" +GROUP_NAME_WITHOUT_SPACE="test.group" +GROUP_NAME_WITH_SPACE="test group" +SECRET_GROUP="private_group" +"$USER_RUN_TESTS_DIR"/create-one-time-test-data.sh --non-secret-user "$NON_SECRET_USER" \ + --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \ + --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" --secret-group "$SECRET_GROUP" echo "Running Task plugin tests ..." -cd "$USER_RUN_TESTS_DIR"/../../ && ./check_task_statuses.sh "$GERRIT_HOST" + +RESULT=0 + +"$USER_RUN_TESTS_DIR"/../../check_task_statuses.sh \ + --server "$GERRIT_HOST" --non-secret-user "$NON_SECRET_USER" \ + --untrusted-user "$UNTRUSTED_USER" --non-secret-group-without-space "$GROUP_NAME_WITHOUT_SPACE" \ + --non-secret-group-with-space "$GROUP_NAME_WITH_SPACE" || RESULT=1 + +"$USER_RUN_TESTS_DIR"/../../check_task_visibility.sh --server "$GERRIT_HOST" \ + --non-secret-user "$NON_SECRET_USER" --non-secret-group "$GROUP_NAME_WITHOUT_SPACE" \ + --secret-group "$SECRET_GROUP" || RESULT=1 + +exit $RESULT
diff --git a/test/docker/run_tests/update-all-users-project.sh b/test/docker/run_tests/update-all-users-project.sh index d0e1527..cfe2def 100755 --- a/test/docker/run_tests/update-all-users-project.sh +++ b/test/docker/run_tests/update-all-users-project.sh
@@ -4,7 +4,13 @@ cd "$WORKSPACE" && git clone ssh://"$GERRIT_HOST":29418/All-Users allusers && cd allusers git fetch origin refs/meta/config && git checkout FETCH_HEAD -git config -f project.config access."refs/users/*".read "group Administrators" git config -f project.config access."refs/users/*".push "group Administrators" -git config -f project.config access."refs/users/*".create "group Administrators" + +git config -f project.config access.'refs/users/${shardeduserid}'.read "group Registered Users" +git config -f project.config access.'refs/users/${shardeduserid}'.push "group Registered Users" +git config -f project.config access.'refs/users/${shardeduserid}'.create "group Registered Users" +git config -f "project.config" \ + access."refs/*".read "deny group Anonymous Users" +echo -e "global:Registered-Users\tRegistered Users" >> groups +echo -e "global:Anonymous-Users\tAnonymous Users" >> groups git add . && git commit -m "project config update" && git push origin HEAD:refs/meta/config
diff --git a/test/lib/lib_helper.sh b/test/lib/lib_helper.sh new file mode 100644 index 0000000..f4006db --- /dev/null +++ b/test/lib/lib_helper.sh
@@ -0,0 +1,350 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ---- TEST RESULTS ---- +result() { # test [error_message] + local result=$? + if [ $result -eq 0 ] ; then + echo "PASSED - $1 test" + else + echo "*** FAILED *** - $1 test" + RESULT=$result + [ $# -gt 1 ] && echo "$2" + fi +} + +# output must match expected to pass +result_out() { # test expected actual + local name=$1 expected=$2 actual=$3 + + [ "$expected" = "$actual" ] + result "$name" "$(diff <(echo "$expected") <(echo "$actual"))" +} + +result_root() { # group root + local name="$1 - $(echo "$2" | sed -es'/Root //')" + result_out "$name" "${EXPECTED_ROOTS[$2]}" "${OUTPUT_ROOTS[$2]}" +} + +# -------- Git Config + +config() { git config -f "$CONFIG" "$@" ; } # [args]... +config_section_keys() { # section > keys ... + # handlers.handler-filter filter.sh -> handler-filter + config -l --name-only |\ + grep "^$1\." | \ + sed -es"/^$1\.//;s/\..*$//" |\ + awk '$0 != prev ; {prev = $0}' +} + +# -------- Pre JSON -------- +# +# pre_json is a "templated json" used in the test docs to express test results. It looks +# like json but has some extra comments to express when a certain output should be used. +# These comments look like: "# Only Test Suite: <suite>" +# + +remove_suites() { # suites... < pre_json > json + grep -vE "# Only Test Suite: ($(echo "$@" | sed "s/ /|/g"))" | \ + sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//' +} + +# pre_json is a "templated json" used in the test docs to express test results. It looks +# like json but has some extra comments to express when a certain output should be used. +# These comments look like: "# Test Suite: <suite>[, <suite>][, <suite>]..." +# + +keep_suites() { # suites... < pre_json > json + grep -E "# Test Suite: (.*, )?($(echo "$@" | sed "s/ /|/g"))(, .*)?$" | \ + sed -e's/# Only Test Suite:.*$//; s/# Test Suite:.*$//; s/ *$//' +} + +remove_not_suite() { remove_suites !"$1" ; } # suite < pre_json > json + +# -------- Test Doc Format -------- +# +# Test Doc Format has intermixed git config task definitions with json roots. This +# makes it easy to define tests close to their outputs. Be aware that all of the +# config will get consolidated into a single file, so non root config will be shared +# amongst all the roots. +# + +# Sample Test Doc for 2 roots: +# +# [root "Root PASS"] +# pass = True +# +# { +# "applicable" : true, +# "hasPass" : true, +# "name" : "Root PASS", +# "status" : "PASS" +# } +# +# [root "Root FAIL"] +# fail = True +# +# { +# <other root> +# } + +# Strip the json from Test Doc formatted text. For the sample above, the output would be: +# +# [root "Root PASS"] +# pass = True +# +# [root "Root FAIL"] +# fail = True +# ... +# +testdoc_2_cfg() { awk '/^\{/,/^$/ { next } ; 1' ; } # testdoc_format > task_config + +# Strip the git config from Test Doc formatted text. For the sample above, the output would be: +# +# { "plugins" : [ +# { "name" : "task", +# "roots" : [ +# { +# "applicable" : true, +# "hasPass" : true, +# "name" : "Root PASS", +# "status" : "PASS" +# }, +# { +# <other root> +# }, +# ... +# } +testdoc_2_pjson() { # < testdoc_format > pjson_task_roots + awk 'BEGIN { print "{ \"plugins\" : [ { \"name\" : \"task\", \"roots\" : [" }; \ + /^\{/ { open=1 }; \ + open && end { print "}," ; end=0 }; \ + /^\}/ { open=0 ; end=1 }; \ + open; \ + END { print "}]}]}" }' +} + +# ---- JSON PARSING ---- + +json_pp() { # < json > json + python -c "import sys, json; \ + print json.dumps(json.loads(sys.stdin.read()), indent=3, \ + separators=(',', ' : '), sort_keys=True)" +} + +json_val_by() { # json index|'key' > value + echo "$1" | python -c "import json,sys;print json.load(sys.stdin)[$2]" +} +json_val_by_key() { json_val_by "$1" "'$2'" ; } # json key > value + +# -------- + +gssh() { # [-l user] cmd [args]... + local user_args=() + [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; } + ssh -x -p "$PORT" "${user_args[@]}" "$SERVER" gerrit "$@" +} + +q() { "$@" > /dev/null 2>&1 ; } # cmd [args...] # quiet a command + +gen_change_id() { echo "I$(uuidgen | sha1sum | awk '{print $1}')"; } # > change_id + +commit_message() { printf "$1 \n\nChange-Id: $2" ; } # message change-id > commit_msg + +err() { echo "ERROR: $1" >&2 ; exit 1 ; } + +# Run a test setup command quietly, exit on failure +q_setup() { local out ; out=$("$@" 2>&1) || err "$out" ; } # cmd [args...] + +ensure() { "$@" || err "$1 results are not valid" ; } # cmd [args]... < data > data + +set_change() { # change_json + { CHANGE=("$(json_val_by_key "$1" number)" \ + "$(json_val_by_key "$1" id)" \ + "$(json_val_by_key "$1" project)" \ + "refs/heads/$(json_val_by_key "$1" branch)" \ + "$(json_val_by_key "$1" status)" \ + "$(json_val_by_key "$1" topic)") ; } 2> /dev/null +} + +# change_token change_number change_id project branch status topic < templated_txt > change_txt +replace_change_properties() { + sed -e "s|_change$1_number|$2|g" \ + -e "s|_change$1_id|$3|g" \ + -e "s|_change$1_project|$4|g" \ + -e "s|_change$1_branch|$5|g" \ + -e "s|_change$1_status|$6|g" \ + -e "s|_change$1_topic|$7|g" +} + +replace_default_changes() { + replace_change_properties "1" "${CHANGE1[@]}" | replace_change_properties "2" "${CHANGE2[@]}" +} + +replace_groups() { # < text_with_groups > test_with_expanded_groups + local text="$(< /dev/stdin)" + for placeholder in "${!GROUP_EXPANDED_BY_PLACEHOLDER[@]}" ; do + text="${text//"$placeholder"/${GROUP_EXPANDED_BY_PLACEHOLDER["$placeholder"]}}" + done + echo "$text" +} + +get_group_uuid() { # group_name > group_uuid + gssh ls-groups -v | awk '-F\t' '$1 == "'"$1"'" {print $2}' +} + +get_sharded_group_uuid() { # group_name > sharded_group_uuid + local group_id=$(get_group_uuid "$1") + echo "${group_id:0:2}/$group_id" +} + +replace_users() { # < text_with_users > test_with_expanded_users + local text="$(< /dev/stdin)" + for user in "${!USERS[@]}" ; do + text="${text//"$user"/${USERS["$user"]}}" + done + echo "$text" +} + +replace_user() { # < text_with_testuser > text_with_$USER + sed -e"s/testuser/$USER/" +} + +get_user_ref() { # username > refs/users/<accountidshard>/<accountid> + local user_account_id="$(curl --netrc --silent "http://$SERVER:$HTTP_PORT/a/accounts/$1" | \ + sed -e '1!b' -e "/^)]}'$/d" | jq ._account_id)" + echo "refs/users/${user_account_id:(-2)}/$user_account_id" +} + +replace_user_refs() { # < text_with_user_refs > test_with_expanded_user_refs + local text="$(< /dev/stdin)" + for user in "${!USER_REFS[@]}" ; do + text="${text//"$user"/${USER_REFS["$user"]}}" + done + echo "$text" +} + +replace_tokens() { # < text > text with replacing all tokens(changes, user) + replace_default_changes | replace_user_refs | replace_user | replace_groups +} + +strip_non_applicable() { ensure "$MYDIR"/strip_non_applicable.py ; } # < json > json +strip_non_invalid() { ensure "$MYDIR"/strip_non_invalid.py ; } # < json > json + +define_jsonByRoot() { # task_plugin_ouptut > jsonByRoot_array_definition + local record root='' + local -A jsonByRoot + while IFS= read -r -d '' record ; do + if [ -z "$root" ] ; then + root=$record + else + jsonByRoot[$root]=$record + root='' + fi + done < <(python -c "if True: # NOP to start indent + import sys, json + + roots=json.loads(sys.stdin.read())['plugins'][0]['roots'] + for root in roots: + root_json = json.dumps(root, indent=3, separators=(',', ' : '), sort_keys=True) + sys.stdout.write(root['name'] + '\x00' + root_json + '\x00')" + ) + + local def=$(declare -p jsonByRoot) + echo "${def#*=}" # declare -A jsonByRoot='(...)' > '(...)' +} + +get_plugins() { # < change_json > plugins_json + python -c "import sys, json; \ + plugins={}; plugins['plugins']=json.loads(sys.stdin.read())['plugins']; \ + print json.dumps(plugins, indent=3, separators=(',', ' : '), sort_keys=True)" +} + +example() { # doc example_num > text_for_example_num + echo "$1" | awk '/```/{Q++;E=(Q+1)/2};E=='"$2" | grep -v '```' +} + +get_change_num() { # < gerrit_push_response > changenum + local url=$(awk '$NF ~ /\[NEW\]/ { print $2 }') + echo "${url##*\/}" | tr -d -c '[:digit:]' +} + +install_changeid_hook() { # repo + local hook=$(git rev-parse --git-dir)/hooks/commit-msg + scp -p -P "$PORT" "$SERVER":hooks/commit-msg "$hook" + chmod +x "$hook" +} + +setup_repo() { # repo remote ref [--initial-commit] + local repo=$1 remote=$2 ref=$3 init=$4 + git init "$repo" + ( + cd "$repo" + install_changeid_hook "$repo" + git fetch "$remote" "$ref" + if ! git checkout FETCH_HEAD ; then + if [ "$init" = "--initial-commit" ] ; then + git commit --allow-empty -a -m "Initial Commit" + fi + fi + ) +} + +update_repo() { # repo remote ref + local repo=$1 remote=$2 ref=$3 + ( + cd "$repo" + git add . + git commit -m 'Testing task plugin' + git push "$remote" HEAD:"$ref" + ) +} + +create_repo_change() { # repo remote ref [change_id] > change_num + local repo=$1 remote=$2 ref=$3 change_id=$4 msg="Test change" + ( + q cd "$repo" + uuidgen > file + q git add . + [ -n "$change_id" ] && msg=$(commit_message "$msg" "$change_id") + q git commit -m "$msg" + git push "$remote" HEAD:"refs/for/$ref" 2>&1 | get_change_num + ) +} + +query() { # [-l user] query > json lines + local user_args=() + [ "-l" = "$1" ] && { user_args=("-l" "$2") ; shift 2 ; } + gssh "${user_args[@]}" query "$@" --format json +} + +# N < json lines > changeN_json +change_plugins() { awk "NR==$1" | get_plugins | json_pp ; } + +results_suite() { # name expected_file plugins_json + local name=$1 expected=$2 actual=$3 + + local -A EXPECTED_ROOTS=$(define_jsonByRoot < "$expected") + local -A OUTPUT_ROOTS=$(echo "$actual" | define_jsonByRoot) + + local out root + echo "$ROOTS" | while read root ; do + result_root "$name" "$root" + done + out=$(diff "$expected" <(echo "$actual") | head -15) + [ -z "$out" ] + result "$name - Full Test Suite" "$out" +}
diff --git a/test/lib/lib_md.sh b/test/lib/lib_md.sh new file mode 100644 index 0000000..2838163 --- /dev/null +++ b/test/lib/lib_md.sh
@@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# +# Copyright (C) 2021 The Android Open Source Project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ---- Markdown Format Helpers ---- + +# Example markdown file: +# (Using block comment to better understand the file syntax.) + +: <<'END' +# Test case description header + +file: `All-Projects.git:refs/meta/config:task.config` +``` +[root "Test root"] + applicable = "is:open" + pass = True +``` + +file: `All-Users:refs/users/some_ref:task/sample.config` +``` + [task "NON-SECRET task"] + applicable = is:open + pass = Fail ++ subtasks-external = SECRET + ++[external "SECRET"] ++ user = {secret_user} ++ file = secret.config +``` + +json: +``` +{ + { + "some": "example" + } +} +END + +# (For example above) +# out: +# `All-Projects.git:refs/meta/config:task.config` +# `All-Users:refs/users/some_ref:task/sample.config` +md_file_markers() { # DOC_CONTENT + echo "$1" | grep -o "^file: .*" | cut -f2 -d'`' +} + +# (For example above) +# in: `All-Projects.git:refs/meta/config:task.config` +# out: +#[root "Test root"] +# applicable = "is:open" +# pass = True +# +# in: json: +# out : +# { +# { +# "some": "example" +# } +# } +md_marker_content() { # DOC marker + local start_line=$(echo "$1" | grep -n "$2" | cut -f1 -d':') + echo "$1" | tail -n+"$start_line" | \ + sed '1,/```/d;/```/,$d' | grep -v '```' +} + +# file_marker > project +# in: `All-Projects.git:refs/meta/config:task/task.config` +# out: All-Projects.git +md_file_marker_project() { + echo "$1" | cut -f1 -d':' +} + +# file_marker > ref +# in: `All-Projects.git:refs/meta/config:task/task.config` +# out: refs/meta/config +md_file_marker_ref() { + echo "$1" | cut -f2 -d':' +} + +# file_marker > file +# in: `All-Projects.git:refs/meta/config:task/task.config` +#out: task/task.config +md_file_marker_file() { + echo "$1" | cut -f3 -d':' +} \ No newline at end of file
diff --git a/test/strip_non_applicable.py b/test/strip_non_applicable.py index 1ff097a..41c21fa 100755 --- a/test/strip_non_applicable.py +++ b/test/strip_non_applicable.py
@@ -43,7 +43,7 @@ status='' if STATUS in task.keys(): status = task[STATUS] - if status != 'INVALID': + if status != 'INVALID' and status != 'DUPLICATE': del tasks[i] nexti = i
diff --git a/tools/bzl/junit.bzl b/tools/bzl/junit.bzl new file mode 100644 index 0000000..5df79bb --- /dev/null +++ b/tools/bzl/junit.bzl
@@ -0,0 +1,6 @@ +load( + "@com_googlesource_gerrit_bazlets//tools:junit.bzl", + _junit_tests = "junit_tests", +) + +junit_tests = _junit_tests \ No newline at end of file
diff --git a/tools/bzl/maven_jar.bzl b/tools/bzl/maven_jar.bzl new file mode 100644 index 0000000..4871c7b --- /dev/null +++ b/tools/bzl/maven_jar.bzl
@@ -0,0 +1,4 @@ +load("@com_googlesource_gerrit_bazlets//tools:maven_jar.bzl", _gerrit = "GERRIT", _maven_jar = "maven_jar") + +maven_jar = _maven_jar +GERRIT = _gerrit \ No newline at end of file
diff --git a/tools/bzl/plugin.bzl b/tools/bzl/plugin.bzl index 89a1643..67536ef 100644 --- a/tools/bzl/plugin.bzl +++ b/tools/bzl/plugin.bzl
@@ -2,7 +2,9 @@ "@com_googlesource_gerrit_bazlets//:gerrit_plugin.bzl", _gerrit_plugin = "gerrit_plugin", _plugin_deps = "PLUGIN_DEPS", + _plugin_test_deps = "PLUGIN_TEST_DEPS", ) gerrit_plugin = _gerrit_plugin PLUGIN_DEPS = _plugin_deps +PLUGIN_TEST_DEPS = _plugin_test_deps \ No newline at end of file
diff --git a/tools/playbooks/install_maven.yaml b/tools/playbooks/install_maven.yaml new file mode 100644 index 0000000..ae1690d --- /dev/null +++ b/tools/playbooks/install_maven.yaml
@@ -0,0 +1,8 @@ +- hosts: all + tasks: + - name: Install maven + become: true + package: + name: + - maven + state: present
diff --git a/tools/playbooks/install_python3-distutils.yaml b/tools/playbooks/install_python3-distutils.yaml new file mode 100644 index 0000000..75f5a2a --- /dev/null +++ b/tools/playbooks/install_python3-distutils.yaml
@@ -0,0 +1,10 @@ +- hosts: all + roles: + - name: ensure-python + tasks: + - name: Install python3-distutils + become: true + package: + name: + - python3-distutils + state: present
diff --git a/tools/workspace_status.py b/tools/workspace_status.py index d948424..fb5ec6d 100644 --- a/tools/workspace_status.py +++ b/tools/workspace_status.py
@@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # This script will be run by bazel when the build process starts to # generate key-value information that represents the status of the
diff --git a/yarn.lock b/yarn.lock index 0bdc3f3..da0237a 100644 --- a/yarn.lock +++ b/yarn.lock
@@ -2,53 +2,91 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" - integrity sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== dependencies: - "@babel/highlight" "^7.12.13" + "@babel/highlight" "^7.10.4" -"@babel/helper-validator-identifier@^7.14.0": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz#d26cad8a47c65286b15df1547319a5d0bcf27288" - integrity sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A== +"@babel/helper-validator-identifier@^7.18.6": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== -"@babel/highlight@^7.12.13": - version "7.14.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.0.tgz#3197e375711ef6bf834e67d0daec88e4f46113cf" - integrity sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg== +"@babel/highlight@^7.10.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== dependencies: - "@babel/helper-validator-identifier" "^7.14.0" + "@babel/helper-validator-identifier" "^7.18.6" chalk "^2.0.0" js-tokens "^4.0.0" -"@bazel/rollup@^3.4.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-3.5.0.tgz#3de2db08cbc62c3cffbbabaa4517ec250cf6419a" - integrity sha512-sFPqbzSbIn6h66uuZdXgK5oitSmEGtnDPfL3TwTS4ZWy75SpYvk9X1TFGlvkralEkVnFfdH15sq80/1t+YgQow== +"@bazel/rollup@~5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-5.1.0.tgz#dc858ddc93c9fdb9cc2e7982e632c939c646ebdc" + integrity sha512-wEiWdSyVbsycSirSYjR6FGfPGbRNI7sGNAYmrV0hIzYIi+KqXeTNcwKIRSE9PESP3mb0VWbZmHvXvmrWk6daPQ== + dependencies: + "@bazel/worker" "5.1.0" -"@bazel/terser@^3.4.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-3.5.0.tgz#4b1c3a3b781e65547694aa05bc600c251e4d8c0b" - integrity sha512-dpWHn1Iu+w0uA/kvPb0pP+4Io0PrVuzCCbVg2Ow4uRt/gTFKQJJWp4EiTitEZlPA2dHlW7PHThAb93lGo2c8qA== +"@bazel/terser@~5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-5.1.0.tgz#5c82b93f4d9def8103c16be2dd33900d156fa066" + integrity sha512-uE3hTqfkZr4nvlk3jwi0xx6URqqI7r6GGPtDAU02/PVei+O4PfThaov7cwHO+D1FnoLncDqChb9Iolr7Crw/8A== + +"@bazel/worker@5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@bazel/worker/-/worker-5.1.0.tgz#6f1e0f3ef628e3449d424cacd341c9abd09a3735" + integrity sha512-u3aU93UtHz3vL6ozezq0jnw83s1cNT4dAnW+vvB7M++YKFlB3CWzZFb0JRJbCp1b6DDe30ML0WOdd3nVYuylpw== + dependencies: + google-protobuf "^3.6.1" + +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -acorn-jsx@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^7.1.1: +acorn@^7.4.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2: +ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -58,31 +96,39 @@ json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== +ajv@^8.0.1: + version "8.11.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== dependencies: - type-fest "^0.21.3" + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== ansi-regex@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== -ansi-styles@^3.2.0, ansi-styles@^3.2.1: +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" -ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -96,30 +142,31 @@ dependencies: sprintf-js "~1.0.2" -array-includes@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.3.tgz#c7f619b382ad2afaf5326cddfdc0afc61af7690a" - integrity sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A== +array-includes@^3.1.4: + version "3.1.5" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.5.tgz#2c320010db8d31031fd2a5f6b3bbd4b1aad31bdb" + integrity sha512-iSDYZMMyTPkiFasVqfuAQnWAYcvO/SeBSCGKePoEthjp4LEMTe4uLc7b025o4jAZpHhihh8xPo99TNWUWWkGDQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.19.5" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array.prototype.flat@^1.2.5: + version "1.3.0" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.0.tgz#0b0c1567bf57b38b56b4c97b8aa72ab45e4adc7b" + integrity sha512-12IUEkHsAhA4DY5s0FPgNXIdc8VRSqD9Zp78a5au9abH/SOBrsp082JOWFNTjkMozh8mqcdiKuaLGhPeYztxSw== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.0-next.2" - get-intrinsic "^1.1.1" - is-string "^1.0.5" + es-abstract "^1.19.2" + es-shim-unscopables "^1.0.0" -array.prototype.flat@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz#6ef638b43312bd401b4c6199fdec7e2dc9e9a123" - integrity sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - es-abstract "^1.18.0-next.1" - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== balanced-match@^1.0.0: version "1.0.2" @@ -152,7 +199,7 @@ resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chalk@^2.0.0, chalk@^2.1.0: +chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -161,31 +208,14 @@ escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad" - integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg== +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== dependencies: ansi-styles "^4.1.0" supports-color "^7.1.0" -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -215,26 +245,24 @@ resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -comment-parser@^0.7.2: - version "0.7.6" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.7.6.tgz#0e743a53c8e646c899a1323db31f6cd337b10f12" - integrity sha512-GKNxVA7/iuTnAqGADlTWX4tkhzxZKXp5fLJqKTlQLHkE65XDUKutZ3BHaJC5IGcper2tT3QRD1xr4o3jNpgXXg== +comment-parser@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.1.5.tgz#453627ef8f67dbcec44e79a9bd5baa37f0bce9b2" + integrity sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA== concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" debug@^2.6.9: version "2.6.9" @@ -264,10 +292,17 @@ dependencies: ms "2.1.2" -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= +debug@^4.3.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== define-properties@^1.1.3: version "1.1.3" @@ -276,6 +311,14 @@ dependencies: object-keys "^1.0.12" +define-properties@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" + integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + dependencies: + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -311,60 +354,79 @@ dependencies: domelementtype "^2.2.0" -domutils@^2.5.2: - version "2.6.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.6.0.tgz#2e15c04185d43fb16ae7057cb76433c6edb938b7" - integrity sha512-y0BezHuy4MDYxh6OvolXYsH+1EMGmFbwv5FKW7ovwMG6zTPWqNPq3WF9ayZssFq+UlKdffGLbOEaghNdaOm1WA== +domhandler@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== dependencies: dom-serializer "^1.0.1" domelementtype "^2.2.0" domhandler "^4.2.0" -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +enquirer@^2.3.5: + version "2.3.6" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" + integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== + dependencies: + ansi-colors "^4.1.1" + entities@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" +entities@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" + integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== -es-abstract@^1.18.0-next.1, es-abstract@^1.18.0-next.2, es-abstract@^1.18.2: - version "1.18.6" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.6.tgz#2c44e3ea7a6255039164d26559777a6d978cb456" - integrity sha512-kAeIT4cku5eNLNuUKhlmtuk1/TRZvQoYccn6TO0cSVdf1kzB0T7+dYuVK9MWM7l+/53W2Q8M7N2c6MQvhXFcUQ== +es-abstract@^1.19.0, es-abstract@^1.19.1, es-abstract@^1.19.2, es-abstract@^1.19.5: + version "1.20.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.20.2.tgz#8495a07bc56d342a3b8ea3ab01bd986700c2ccb3" + integrity sha512-XxXQuVNrySBNlEkTYJoDNFe5+s2yIOpzq80sUHEdPdQr0S5nTLz4ZPPPswNIpKseDDUS5yghX1gfLIHQZ1iNuQ== dependencies: call-bind "^1.0.2" es-to-primitive "^1.2.1" function-bind "^1.1.1" - get-intrinsic "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.1.2" get-symbol-description "^1.0.0" has "^1.0.3" - has-symbols "^1.0.2" + has-property-descriptors "^1.0.0" + has-symbols "^1.0.3" internal-slot "^1.0.3" is-callable "^1.2.4" - is-negative-zero "^2.0.1" + is-negative-zero "^2.0.2" is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" is-string "^1.0.7" - object-inspect "^1.11.0" + is-weakref "^1.0.2" + object-inspect "^1.12.2" object-keys "^1.1.1" - object.assign "^4.1.2" - string.prototype.trimend "^1.0.4" - string.prototype.trimstart "^1.0.4" - unbox-primitive "^1.0.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.4.3" + string.prototype.trimend "^1.0.5" + string.prototype.trimstart "^1.0.5" + unbox-primitive "^1.0.2" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" es-to-primitive@^1.2.1: version "1.2.1" @@ -380,10 +442,15 @@ resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -eslint-config-google@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.13.0.tgz#e277d16d2cb25c1ffd3fd13fb0035ad7421382fe" - integrity sha512-ELgMdOIpn0CFdsQS+FuxO+Ttu4p+aLaXHv9wA9yVnzqlUGV7oN/eRRnJekk7TCur6Cu2FXX0fqfIXRBaM14lpQ== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-google@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz#4f5f8759ba6e11b424294a219dbfa18c508bcc1a" + integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw== eslint-import-resolver-node@^0.3.6: version "0.3.6" @@ -393,57 +460,53 @@ debug "^3.2.7" resolve "^1.20.0" -eslint-module-utils@^2.6.2: - version "2.6.2" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz#94e5540dd15fe1522e8ffa3ec8db3b7fa7e7a534" - integrity sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q== +eslint-module-utils@^2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz#4f3e41116aaf13a20792261e61d3a2e7e0583974" + integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== dependencies: debug "^3.2.7" - pkg-dir "^2.0.0" -eslint-plugin-html@^6.0.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.1.2.tgz#fa26e4804428956c80e963b6499c192061c2daf3" - integrity sha512-bhBIRyZFqI4EoF12lGDHAmgfff8eLXx6R52/K3ESQhsxzCzIE6hdebS7Py651f7U3RBotqroUnC3L29bR7qJWQ== +eslint-plugin-html@^6.1.2: + version "6.2.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-html/-/eslint-plugin-html-6.2.0.tgz#715bc00b50bbd0d996e28f953c289a5ebec69d43" + integrity sha512-vi3NW0E8AJombTvt8beMwkL1R/fdRWl4QSNRNMhVQKWm36/X0KF0unGNAY4mqUF06mnwVWZcIcerrCnfn9025g== dependencies: - htmlparser2 "^6.0.1" + htmlparser2 "^7.1.2" -eslint-plugin-import@^2.20.1: - version "2.24.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da" - integrity sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q== +eslint-plugin-import@^2.22.1: + version "2.26.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz#f812dc47be4f2b72b478a021605a59fc6fe8b88b" + integrity sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA== dependencies: - array-includes "^3.1.3" - array.prototype.flat "^1.2.4" + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" debug "^2.6.9" doctrine "^2.1.0" eslint-import-resolver-node "^0.3.6" - eslint-module-utils "^2.6.2" - find-up "^2.0.0" + eslint-module-utils "^2.7.3" has "^1.0.3" - is-core-module "^2.6.0" - minimatch "^3.0.4" - object.values "^1.1.4" - pkg-up "^2.0.0" - read-pkg-up "^3.0.0" - resolve "^1.20.0" - tsconfig-paths "^3.11.0" + is-core-module "^2.8.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.values "^1.1.5" + resolve "^1.22.0" + tsconfig-paths "^3.14.1" -eslint-plugin-jsdoc@^19.2.0: - version "19.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-19.2.0.tgz#f522b970878ae402b28ce62187305b33dfe2c834" - integrity sha512-QdNifBFLXCDGdy+26RXxcrqzEZarFWNybCZQVqJQYEYPlxd6lm+LPkrs6mCOhaGc2wqC6zqpedBQFX8nQJuKSw== +eslint-plugin-jsdoc@^32.3.0: + version "32.3.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-32.3.4.tgz#6888f3b2dbb9f73fb551458c639a4e8c84fe9ddc" + integrity sha512-xSWfsYvffXnN0OkwLnB7MoDDDDjqcp46W7YlY1j7JyfAQBQ+WnGCfLov3gVNZjUGtK9Otj8mEhTZTqJu4QtIGA== dependencies: - comment-parser "^0.7.2" - debug "^4.1.1" - jsdoctypeparser "^6.1.0" - lodash "^4.17.15" - object.entries-ponyfill "^1.0.1" - regextras "^0.7.0" - semver "^6.3.0" - spdx-expression-parse "^3.0.0" + comment-parser "1.1.5" + debug "^4.3.1" + jsdoctypeparser "^9.0.0" + lodash "^4.17.21" + regextras "^0.7.1" + semver "^7.3.5" + spdx-expression-parse "^3.0.1" -eslint-scope@^5.0.0: +eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -451,76 +514,84 @@ esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-utils@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" - integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q== +eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== dependencies: eslint-visitor-keys "^1.1.0" -eslint-visitor-keys@^1.1.0: +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== -eslint@^6.6.0: - version "6.8.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" - integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig== +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.24.0: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== dependencies: - "@babel/code-frame" "^7.0.0" + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" + chalk "^4.0.0" + cross-spawn "^7.0.2" debug "^4.0.1" doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.3" - eslint-visitor-keys "^1.1.0" - espree "^6.1.2" - esquery "^1.0.1" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" esutils "^2.0.2" - file-entry-cache "^5.0.1" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^12.1.0" + glob-parent "^5.1.2" + globals "^13.6.0" ignore "^4.0.6" import-fresh "^3.0.0" imurmurhash "^0.1.4" - inquirer "^7.0.0" is-glob "^4.0.0" js-yaml "^3.13.1" json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" + levn "^0.4.1" + lodash.merge "^4.6.2" minimatch "^3.0.4" - mkdirp "^0.5.1" natural-compare "^1.4.0" - optionator "^0.8.3" + optionator "^0.9.1" progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" text-table "^0.2.0" v8-compile-cache "^2.0.3" -espree@^6.1.2: - version "6.2.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" - integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== dependencies: - acorn "^7.1.1" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.1.0" + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" esprima@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.0.1: +esquery@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== @@ -549,16 +620,7 @@ resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== @@ -568,45 +630,30 @@ resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -figures@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== dependencies: - escape-string-regexp "^1.0.5" + flat-cache "^3.0.4" -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== dependencies: - flat-cache "^2.0.1" + flatted "^3.1.0" + rimraf "^3.0.2" -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" - integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.1.0: + version "3.2.7" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" + integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== fs.realpath@^1.0.0: version "1.0.0" @@ -623,11 +670,26 @@ resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +functions-have-names@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" @@ -637,6 +699,15 @@ has "^1.0.3" has-symbols "^1.0.1" +get-intrinsic@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + get-symbol-description@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" @@ -645,7 +716,7 @@ call-bind "^1.0.2" get-intrinsic "^1.1.1" -glob-parent@^5.0.0: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -664,23 +735,28 @@ once "^1.3.0" path-is-absolute "^1.0.0" -globals@^12.1.0: - version "12.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-12.4.0.tgz#a18813576a41b00a24a97e7f815918c2e19925f8" - integrity sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg== +globals@^13.6.0, globals@^13.9.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== dependencies: - type-fest "^0.8.1" + type-fest "^0.20.2" -graceful-fs@^4.1.2: - version "4.2.8" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a" - integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg== +google-protobuf@^3.6.1: + version "3.21.2" + resolved "https://registry.yarnpkg.com/google-protobuf/-/google-protobuf-3.21.2.tgz#4580a2bea8bbb291ee579d1fefb14d6fa3070ea4" + integrity sha512-3MSOYFO5U9mPGikIYCzK0SaThypfGgS6bHqrUGXG3DPHCrb+txNqeEcns1W0lkGfk0rCyNXm7xB9rMxnCiZOoA== has-bigints@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== +has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -691,11 +767,23 @@ resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== +has-property-descriptors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" + integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + dependencies: + get-intrinsic "^1.1.1" + has-symbols@^1.0.1, has-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + has-tostringtag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" @@ -710,34 +798,22 @@ dependencies: function-bind "^1.1.1" -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - -htmlparser2@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== +htmlparser2@^7.1.2: + version "7.2.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-7.2.0.tgz#8817cdea38bbc324392a90b1990908e81a65f5a5" + integrity sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog== dependencies: domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" + domhandler "^4.2.2" + domutils "^2.8.0" + entities "^3.0.1" ignore@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -import-fresh@^3.0.0: +import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -763,25 +839,6 @@ resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -inquirer@^7.0.0: - version "7.3.3" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" - integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.19" - mute-stream "0.0.8" - run-async "^2.4.0" - rxjs "^6.6.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - internal-slot@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" @@ -791,11 +848,6 @@ has "^1.0.3" side-channel "^1.0.4" -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - is-bigint@^1.0.1: version "1.0.4" resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" @@ -816,13 +868,20 @@ resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== -is-core-module@^2.2.0, is-core-module@^2.6.0: +is-core-module@^2.2.0: version "2.6.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.6.0.tgz#d7553b2526fe59b92ba3e40c8df757ec8a709e19" integrity sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ== dependencies: has "^1.0.3" +is-core-module@^2.8.1, is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.5" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" @@ -835,11 +894,6 @@ resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - is-fullwidth-code-point@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" @@ -852,10 +906,17 @@ dependencies: is-extglob "^2.1.1" -is-negative-zero@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.1.tgz#3de746c18dda2319241a53675908d8f766f11c24" - integrity sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w== +is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: version "1.0.6" @@ -872,6 +933,13 @@ call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + is-string@^1.0.5, is-string@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" @@ -886,6 +954,13 @@ dependencies: has-symbols "^1.0.2" +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -904,21 +979,21 @@ argparse "^1.0.7" esprima "^4.0.0" -jsdoctypeparser@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-6.1.0.tgz#acfb936c26300d98f1405cb03e20b06748e512a8" - integrity sha512-UCQBZ3xCUBv/PLfwKAJhp6jmGOSLFNKzrotXGNgbKhWvz27wPsCsVeP7gIcHPElQw2agBmynAitXqhxR58XAmA== - -json-parse-better-errors@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" - integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +jsdoctypeparser@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz#8c97e2fb69315eb274b0f01377eaa5c940bd7b26" + integrity sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw== json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -931,41 +1006,35 @@ dependencies: minimist "^1.2.0" -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" + prelude-ls "^1.2.1" + type-check "~0.4.0" -load-json-file@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" - integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= - dependencies: - graceful-fs "^4.1.2" - parse-json "^4.0.0" - pify "^3.0.0" - strip-bom "^3.0.0" +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== -lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19: +lodash@^4.17.21: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" minimatch@^3.0.4: version "3.0.4" @@ -974,17 +1043,22 @@ dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.5: +minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== -mkdirp@^0.5.1: - version "0.5.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" - integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== - dependencies: - minimist "^1.2.5" +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== ms@2.0.0: version "2.0.0" @@ -1001,32 +1075,17 @@ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +object-inspect@^1.12.2: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -object-inspect@^1.11.0, object-inspect@^1.9.0: +object-inspect@^1.9.0: version "1.11.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.11.0.tgz#9dceb146cedd4148a0d9e51ab88d34cf509922b1" integrity sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg== @@ -1036,29 +1095,24 @@ resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" - integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - has-symbols "^1.0.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries-ponyfill@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/object.entries-ponyfill/-/object.entries-ponyfill-1.0.1.tgz#29abdf77cbfbd26566dd1aa24e9d88f65433d256" - integrity sha1-Kavfd8v70mVm3RqiTp2I9lQz0lY= - -object.values@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.4.tgz#0d273762833e816b693a637d30073e7051535b30" - integrity sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg== +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== dependencies: call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.18.2" + es-abstract "^1.19.1" once@^1.3.0: version "1.4.0" @@ -1067,48 +1121,17 @@ dependencies: wrappy "1" -onetime@^5.1.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== dependencies: - mimic-fn "^2.1.0" - -optionator@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" parent-module@^1.0.0: version "1.0.1" @@ -1117,64 +1140,25 @@ dependencies: callsites "^3.0.0" -parse-json@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" - integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= - dependencies: - error-ex "^1.3.1" - json-parse-better-errors "^1.0.1" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.6: +path-parse@^1.0.6, path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-type@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" - integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== - dependencies: - pify "^3.0.0" - -pify@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" - integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-2.0.0.tgz#c819ac728059a461cab1c3889a2be3c49a004d7f" - integrity sha1-yBmscoBZpGHKscOImivjxJoATX8= - dependencies: - find-up "^2.1.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== progress@^2.0.0: version "2.0.3" @@ -1186,39 +1170,36 @@ resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= +regexp.prototype.flags@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" + integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" + call-bind "^1.0.2" + define-properties "^1.1.3" + functions-have-names "^1.2.2" -read-pkg@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" - integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= - dependencies: - load-json-file "^4.0.0" - normalize-package-data "^2.3.2" - path-type "^3.0.0" +regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -regextras@^0.7.0: +regextras@^0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/regextras/-/regextras-0.7.1.tgz#be95719d5f43f9ef0b9fa07ad89b7c606995a3b2" integrity sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.10.0, resolve@^1.20.0: +resolve@^1.20.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -1226,18 +1207,19 @@ is-core-module "^2.2.0" path-parse "^1.0.6" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== +resolve@^1.22.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" @@ -1248,44 +1230,24 @@ optionalDependencies: fsevents "~2.3.1" -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -rxjs@^6.6.0: - version "6.6.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== +semver@^7.2.1, semver@^7.3.5: + version "7.3.7" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== dependencies: - tslib "^1.9.0" + lru-cache "^6.0.0" -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -"semver@2 || 3 || 4 || 5", semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== dependencies: - shebang-regex "^1.0.0" + shebang-regex "^3.0.0" -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== side-channel@^1.0.4: version "1.0.4" @@ -1296,19 +1258,14 @@ get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c" - integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" source-map-support@~0.5.19: version "0.5.19" @@ -1328,20 +1285,12 @@ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== -spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - spdx-exceptions@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== -spdx-expression-parse@^3.0.0: +spdx-expression-parse@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== @@ -1359,46 +1308,32 @@ resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -string-width@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string-width@^4.1.0: - version "4.2.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.2.tgz#dafd4f9559a7585cfba529c6a0a4f73488ebd4c5" - integrity sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA== +string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== dependencies: emoji-regex "^8.0.0" is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.0" + strip-ansi "^6.0.1" -string.prototype.trimend@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" - integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== +string.prototype.trimend@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz#914a65baaab25fbdd4ee291ca7dde57e869cb8d0" + integrity sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" + define-properties "^1.1.4" + es-abstract "^1.19.5" -string.prototype.trimstart@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" - integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== +string.prototype.trimstart@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz#5466d93ba58cfa2134839f81d7f42437e8c01fef" + integrity sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg== dependencies: call-bind "^1.0.2" - define-properties "^1.1.3" - -strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" + define-properties "^1.1.4" + es-abstract "^1.19.5" strip-ansi@^6.0.0: version "6.0.0" @@ -1407,12 +1342,19 @@ dependencies: ansi-regex "^5.0.0" +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-bom@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= -strip-json-comments@^3.0.1: +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -1431,15 +1373,21 @@ dependencies: has-flag "^4.0.0" -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +table@^6.0.9: + version "6.8.0" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" + integrity sha512-s/fitrbVeEyHKFa7mFdkuQMWlH1Wgw/yEXMt5xACT4ZpzWFluehAxRtUUQKPuWhaLAWhFcVx6w3oC8VKaUfPGA== dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" terser@^5.6.1: version "5.7.0" @@ -1455,58 +1403,36 @@ resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tsconfig-paths@^3.11.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz#954c1fe973da6339c78e06b03ce2e48810b65f36" - integrity sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA== +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" - minimist "^1.2.0" + minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== dependencies: - prelude-ls "~1.1.2" + prelude-ls "^1.2.1" -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - -unbox-primitive@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" - integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== dependencies: - function-bind "^1.1.1" - has-bigints "^1.0.1" - has-symbols "^1.0.2" + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" uri-js@^4.2.2: @@ -1521,14 +1447,6 @@ resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" @@ -1540,14 +1458,14 @@ is-string "^1.0.5" is-symbol "^1.0.3" -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: +word-wrap@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== @@ -1557,9 +1475,7 @@ resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==