Merge branch 'stable-3.1'
* stable-3.1: Update git submodules Update git submodules gr-rest-api-interface.js: Remove unnecesary Promise resolution LuceneQueryChangesTest: Remove unneeded override of visible() e2e-tests: Rename the now reused json filename constant e2e-tests: Refactor documentation about functional RevisionActions: Do not alter server response e2e-tests: Move core json files into scala package e2e-tests: Support adding/running non-core scenarios e2e-tests: Add create/delete project to CloneUsingBothProtocols ChangeQueryBuilder: Throw error on ambiguous visibleto by display name Change-Id: Idbb499461ecad11da49b3bcbe5d5bf14e90bfe31
This commit is contained in:
@@ -1,29 +1,38 @@
|
|||||||
:linkattrs:
|
:linkattrs:
|
||||||
= Gerrit Code Review - End to end load tests
|
= Gerrit Code Review - End to end tests
|
||||||
|
|
||||||
This document provides a description of a Gerrit load test scenario implemented using the
|
This document provides descriptions of Gerrit end-to-end (`e2e`) test scenarios implemented using
|
||||||
link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
|
link:https://gatling.io/[Gatling,role=external,window=_blank] framework.
|
||||||
|
|
||||||
Similar scenarios have been successfully used to compare performance of different Gerrit versions
|
Similar scenarios have been successfully used to compare performance of different Gerrit versions
|
||||||
or study the Gerrit response under different load profiles.
|
or study the Gerrit response under different load profiles. Although mostly for load, scenarios can
|
||||||
|
either be for link:https://gatling.io/load-testing-continuous-integration/[load or functional,role=external,window=_blank]
|
||||||
|
(e2e) testing purposes. Functional scenarios may then reuse this framework and Gatling's usability
|
||||||
|
features such as its protocols (more below) and
|
||||||
|
link:https://en.wikipedia.org/wiki/Domain-specific_language[DSL,role=external,window=_blank].
|
||||||
|
|
||||||
|
That cross test-scope reusability applies to both Gerrit core scenarios and non-core ones, such as
|
||||||
|
for Gerrit plugins or other potential extensions. End-to-end testing may then include scopes like
|
||||||
|
feature integration, deployment, smoke (and load) testing. These load and functional test scopes
|
||||||
|
should remain orthogonal to the unit and component (aka Gerrit `IT`-suffixed or `acceptance`) ones.
|
||||||
|
The term `acceptance` though may still be coined by organizations to target e2e functional testing.
|
||||||
|
|
||||||
== What is Gatling?
|
== What is Gatling?
|
||||||
|
|
||||||
Gatling is a load testing tool which provides out of the box support for the HTTP protocol.
|
Gatling is mostly a load testing tool which provides out of the box support for the HTTP protocol.
|
||||||
Documentation on how to write an HTTP load test can be found
|
Documentation on how to write an HTTP load test can be found
|
||||||
link:https://gatling.io/docs/current/http/http_protocol/[here,role=external,window=_blank].
|
link:https://gatling.io/docs/current/http/http_protocol/[here,role=external,window=_blank].
|
||||||
|
However, in the scenarios that were initially proposed, the
|
||||||
However, in the scenario we are proposing, we are leveraging the
|
link:https://github.com/GerritForge/gatling-git[Gatling Git extension,role=external,window=_blank] was
|
||||||
link:https://github.com/GerritForge/gatling-git[Gatling Git extension,role=external,window=_blank]
|
leveraged to run tests at the Git protocol level.
|
||||||
to run tests at Git protocol level.
|
|
||||||
|
|
||||||
Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
|
Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
|
||||||
implementation easy even without any Scala knowledge. The
|
implementation easy even without any Scala knowledge. The
|
||||||
link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling,role=external,window=_blank]
|
link:https://gitenterprise.me/2019/12/20/stress-your-gerrit-with-gatling/[Stress your Gerrit with Gatling,role=external,window=_blank]
|
||||||
blog post has more introductory information.
|
blog post has more introductory information.
|
||||||
|
|
||||||
Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory
|
Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory should
|
||||||
should be formatted using the mainstream
|
be formatted using the mainstream
|
||||||
link:https://plugins.jetbrains.com/plugin/1347-scala[Scala plugin for IntelliJ,role=external,window=_blank].
|
link:https://plugins.jetbrains.com/plugin/1347-scala[Scala plugin for IntelliJ,role=external,window=_blank].
|
||||||
The latter is not mandatory but preferred for `sbt` and Scala IDE purposes in this project.
|
The latter is not mandatory but preferred for `sbt` and Scala IDE purposes in this project.
|
||||||
|
|
||||||
@@ -86,10 +95,11 @@ ssh-keyscan -t rsa -p 29418 localhost > ~/.ssh/known_hosts
|
|||||||
=== Input file
|
=== Input file
|
||||||
|
|
||||||
The `CloneUsingBothProtocols` scenario is fed with the data coming from the
|
The `CloneUsingBothProtocols` scenario is fed with the data coming from the
|
||||||
`src/test/resources/data/CloneUsingBothProtocols.json` file. Such a file contains the commands and
|
`src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json` file. Such a
|
||||||
repository used during the load test. That file currently looks like below. This scenario serves
|
file contains the commands and repository used during the e2e test. That file currently looks like
|
||||||
as a simple example with no actual load in it. It can be used to test or validate the local setup.
|
below. This scenario serves as a simple example with no actual load in it. It can be used to test
|
||||||
More complex scenarios can be further developed, under the `com.google.gerrit.scenarios` package.
|
or validate the local setup. More complex scenarios can be further developed, under the
|
||||||
|
`com.google.gerrit.scenarios` package.
|
||||||
|
|
||||||
----
|
----
|
||||||
[
|
[
|
||||||
@@ -111,9 +121,19 @@ Valid commands are:
|
|||||||
* `pull`
|
* `pull`
|
||||||
* `push`
|
* `push`
|
||||||
|
|
||||||
|
=== Project and HTTP credentials
|
||||||
|
|
||||||
The example above assumes that the `loadtest-repo` project exists in the Gerrit under test. The
|
The example above assumes that the `loadtest-repo` project exists in the Gerrit under test. The
|
||||||
`HTTP Credentials` or password obtained from test user's `Settings` (in Gerrit) may be required, in
|
`CloneUsingBothProtocols` scenario already includes creating that project and deleting it once done
|
||||||
`src/test/resources/application.conf`, depending on the above commands used.
|
with it. That scenario class can be used as an example of how a scenario can compose itself
|
||||||
|
alongside other scenarios (here, `CreateProject` and `DeleteProject`).
|
||||||
|
|
||||||
|
The `HTTP Credentials` or password obtained from test user's `Settings` (in Gerrit) may be
|
||||||
|
required, in `src/test/resources/application.conf`, depending on the above commands used. That
|
||||||
|
file's `http` section shows which shell environment variables can be used to set those credentials.
|
||||||
|
|
||||||
|
Executing the `CloneUsingBothProtocols` scenario, as is, does require setting the http credentials.
|
||||||
|
That is because of the aforementioned create/delete project (http) scenarios composed within it.
|
||||||
|
|
||||||
== How to run tests
|
== How to run tests
|
||||||
|
|
||||||
@@ -142,6 +162,27 @@ Gatling's logging level.
|
|||||||
docker run -it e2e-tests -s com.google.gerrit.scenarios.CloneUsingBothProtocols
|
docker run -it e2e-tests -s com.google.gerrit.scenarios.CloneUsingBothProtocols
|
||||||
----
|
----
|
||||||
|
|
||||||
|
=== How to run non-core scenarios
|
||||||
|
|
||||||
|
Locally adding non-core scenarios, for example from Gerrit plugins, is as simple as copying such
|
||||||
|
files in. Copying is necessary over linking, unless running using Docker (above) is not required.
|
||||||
|
Docker does not support links for files it has to copy over through the Dockerfile (here, the
|
||||||
|
scenario files). Here is how to proceed for adding such external (e.g., plugin) scenario files in:
|
||||||
|
|
||||||
|
----
|
||||||
|
pushd e2e-tests/src/test/scala
|
||||||
|
cp -r (or, ln -s) scalaPackageStructure .
|
||||||
|
popd
|
||||||
|
|
||||||
|
pushd e2e-tests/src/test/resources/data
|
||||||
|
cp -r (or, ln -s) jsonFilesPackageStructure .
|
||||||
|
popd
|
||||||
|
----
|
||||||
|
|
||||||
|
The destination folders above readily git-ignore every non-core scenario file added under them. If
|
||||||
|
running using Docker, `e2e-tests/Dockerfile` may require another `COPY` line for the hereby added
|
||||||
|
scenarios. Aforementioned `sbt` or `docker` commands can then be used to run the added tests.
|
||||||
|
|
||||||
GERRIT
|
GERRIT
|
||||||
------
|
------
|
||||||
Part of link:index.html[Gerrit Code Review]
|
Part of link:index.html[Gerrit Code Review]
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ see <<dev-bazel#tests,Running Unit Tests with Bazel>>.
|
|||||||
[[e2e]]
|
[[e2e]]
|
||||||
=== End-to-end tests
|
=== End-to-end tests
|
||||||
|
|
||||||
<<dev-e2e-tests#,This document>> describes how load test scenarios are
|
<<dev-e2e-tests#,This document>> describes how `e2e` (load or functional) test
|
||||||
implemented using link:https://gatling.io/[`Gatling`].
|
scenarios are implemented using link:https://gatling.io/[`Gatling`,role=external,window=_blank].
|
||||||
|
|
||||||
|
|
||||||
== Local server
|
== Local server
|
||||||
|
|||||||
4
e2e-tests/src/test/resources/data/.gitignore
vendored
Normal file
4
e2e-tests/src/test/resources/data/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!*/
|
||||||
|
!/com/google/gerrit/scenarios/*
|
||||||
|
!/.gitignore
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080/a/projects/loadtest-repo"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"url": "http://localhost:8080/a/projects/loadtest-repo/delete-project~delete"
|
||||||
|
}
|
||||||
|
]
|
||||||
4
e2e-tests/src/test/scala/.gitignore
vendored
Normal file
4
e2e-tests/src/test/scala/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
*
|
||||||
|
!*/
|
||||||
|
!/com/google/gerrit/scenarios/*
|
||||||
|
!/.gitignore
|
||||||
@@ -15,18 +15,32 @@
|
|||||||
package com.google.gerrit.scenarios
|
package com.google.gerrit.scenarios
|
||||||
|
|
||||||
import io.gatling.core.Predef._
|
import io.gatling.core.Predef._
|
||||||
|
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||||
import io.gatling.core.structure.ScenarioBuilder
|
import io.gatling.core.structure.ScenarioBuilder
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
class CloneUsingBothProtocols extends GitSimulation {
|
class CloneUsingBothProtocols extends GitSimulation {
|
||||||
|
private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
|
||||||
|
|
||||||
private val test: ScenarioBuilder = scenario(name)
|
private val test: ScenarioBuilder = scenario(name)
|
||||||
.feed(data)
|
.feed(data)
|
||||||
.exec(request)
|
.exec(gitRequest)
|
||||||
|
|
||||||
|
private val createProject = new CreateProject
|
||||||
|
private val deleteProject = new DeleteProject
|
||||||
|
|
||||||
setUp(
|
setUp(
|
||||||
|
createProject.test.inject(
|
||||||
|
atOnceUsers(1)
|
||||||
|
),
|
||||||
test.inject(
|
test.inject(
|
||||||
|
nothingFor(1 second),
|
||||||
constantUsersPerSec(1) during (2 seconds)
|
constantUsersPerSec(1) during (2 seconds)
|
||||||
)).protocols(protocol)
|
),
|
||||||
|
deleteProject.test.inject(
|
||||||
|
nothingFor(3 second),
|
||||||
|
atOnceUsers(1)
|
||||||
|
),
|
||||||
|
).protocols(gitProtocol, httpProtocol)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 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.google.gerrit.scenarios
|
||||||
|
|
||||||
|
import io.gatling.core.Predef._
|
||||||
|
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||||
|
import io.gatling.core.structure.ScenarioBuilder
|
||||||
|
|
||||||
|
class CreateProject extends GerritSimulation {
|
||||||
|
private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
|
||||||
|
|
||||||
|
val test: ScenarioBuilder = scenario(name)
|
||||||
|
.feed(data)
|
||||||
|
.exec(httpRequest)
|
||||||
|
|
||||||
|
setUp(
|
||||||
|
test.inject(
|
||||||
|
atOnceUsers(1)
|
||||||
|
)).protocols(httpProtocol)
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// 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.google.gerrit.scenarios
|
||||||
|
|
||||||
|
import io.gatling.core.Predef._
|
||||||
|
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||||
|
import io.gatling.core.structure.ScenarioBuilder
|
||||||
|
|
||||||
|
class DeleteProject extends GerritSimulation {
|
||||||
|
private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
|
||||||
|
|
||||||
|
val test: ScenarioBuilder = scenario(name)
|
||||||
|
.feed(data)
|
||||||
|
.exec(httpRequest)
|
||||||
|
|
||||||
|
setUp(
|
||||||
|
test.inject(
|
||||||
|
atOnceUsers(1)
|
||||||
|
)).protocols(httpProtocol)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// 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.google.gerrit.scenarios
|
||||||
|
|
||||||
|
import com.github.barbasa.gatling.git.GatlingGitConfiguration
|
||||||
|
import io.gatling.core.Predef._
|
||||||
|
import io.gatling.http.Predef.http
|
||||||
|
import io.gatling.http.protocol.HttpProtocolBuilder
|
||||||
|
import io.gatling.http.request.builder.HttpRequestBuilder
|
||||||
|
|
||||||
|
class GerritSimulation extends Simulation {
|
||||||
|
implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
|
||||||
|
|
||||||
|
private val path: String = this.getClass.getPackage.getName.replaceAllLiterally(".", "/")
|
||||||
|
protected val name: String = this.getClass.getSimpleName
|
||||||
|
protected val resource: String = s"data/$path/$name.json"
|
||||||
|
|
||||||
|
protected val httpRequest: HttpRequestBuilder = http(name).post("${url}")
|
||||||
|
protected val httpProtocol: HttpProtocolBuilder = http.basicAuth(
|
||||||
|
conf.httpConfiguration.userName,
|
||||||
|
conf.httpConfiguration.password)
|
||||||
|
}
|
||||||
@@ -16,23 +16,18 @@ package com.google.gerrit.scenarios
|
|||||||
|
|
||||||
import java.io.{File, IOException}
|
import java.io.{File, IOException}
|
||||||
|
|
||||||
|
import com.github.barbasa.gatling.git.GitRequestSession
|
||||||
import com.github.barbasa.gatling.git.protocol.GitProtocol
|
import com.github.barbasa.gatling.git.protocol.GitProtocol
|
||||||
import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
|
import com.github.barbasa.gatling.git.request.builder.GitRequestBuilder
|
||||||
import com.github.barbasa.gatling.git.{GatlingGitConfiguration, GitRequestSession}
|
|
||||||
import io.gatling.core.Predef._
|
import io.gatling.core.Predef._
|
||||||
import io.gatling.core.feeder.FileBasedFeederBuilder
|
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.eclipse.jgit.hooks.CommitMsgHook
|
import org.eclipse.jgit.hooks.CommitMsgHook
|
||||||
|
|
||||||
class GitSimulation extends Simulation {
|
class GitSimulation extends GerritSimulation {
|
||||||
|
|
||||||
implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
|
|
||||||
implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
|
implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
|
||||||
|
|
||||||
protected val name: String = this.getClass.getSimpleName
|
protected val gitRequest = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
|
||||||
protected val data: FileBasedFeederBuilder[Any]#F = jsonFile(s"data/$name.json").circular
|
protected val gitProtocol: GitProtocol = GitProtocol()
|
||||||
protected val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
|
|
||||||
protected val protocol: GitProtocol = GitProtocol()
|
|
||||||
|
|
||||||
after {
|
after {
|
||||||
Thread.sleep(5000)
|
Thread.sleep(5000)
|
||||||
|
|||||||
@@ -15,16 +15,18 @@
|
|||||||
package com.google.gerrit.scenarios
|
package com.google.gerrit.scenarios
|
||||||
|
|
||||||
import io.gatling.core.Predef._
|
import io.gatling.core.Predef._
|
||||||
|
import io.gatling.core.feeder.FileBasedFeederBuilder
|
||||||
import io.gatling.core.structure.ScenarioBuilder
|
import io.gatling.core.structure.ScenarioBuilder
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
|
||||||
class ReplayRecordsFromFeeder extends GitSimulation {
|
class ReplayRecordsFromFeeder extends GitSimulation {
|
||||||
|
private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).circular
|
||||||
|
|
||||||
private val test: ScenarioBuilder = scenario(name)
|
private val test: ScenarioBuilder = scenario(name)
|
||||||
.repeat(10000) {
|
.repeat(10000) {
|
||||||
feed(data)
|
feed(data)
|
||||||
.exec(request)
|
.exec(gitRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
setUp(
|
setUp(
|
||||||
@@ -34,6 +36,6 @@ class ReplayRecordsFromFeeder extends GitSimulation {
|
|||||||
rampUsers(10) during (5 seconds),
|
rampUsers(10) during (5 seconds),
|
||||||
constantUsersPerSec(20) during (15 seconds),
|
constantUsersPerSec(20) during (15 seconds),
|
||||||
constantUsersPerSec(20) during (15 seconds) randomized
|
constantUsersPerSec(20) during (15 seconds) randomized
|
||||||
)).protocols(protocol)
|
)).protocols(gitProtocol)
|
||||||
.maxDuration(60 seconds)
|
.maxDuration(60 seconds)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
package com.google.gerrit.server.query.change;
|
package com.google.gerrit.server.query.change;
|
||||||
|
|
||||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
|
||||||
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
|
import static com.google.gerrit.entities.Change.CHANGE_ID_PATTERN;
|
||||||
import static com.google.gerrit.server.account.AccountResolver.isSelf;
|
import static com.google.gerrit.server.account.AccountResolver.isSelf;
|
||||||
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
|
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
|
||||||
@@ -24,6 +23,7 @@ import static java.util.stream.Collectors.toSet;
|
|||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.base.Enums;
|
import com.google.common.base.Enums;
|
||||||
import com.google.common.base.Splitter;
|
import com.google.common.base.Splitter;
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.flogger.FluentLogger;
|
import com.google.common.flogger.FluentLogger;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
@@ -985,18 +985,23 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
|
|||||||
if (isSelf(who)) {
|
if (isSelf(who)) {
|
||||||
return isVisible();
|
return isVisible();
|
||||||
}
|
}
|
||||||
|
Set<Account.Id> accounts = null;
|
||||||
try {
|
try {
|
||||||
return Predicate.or(
|
accounts = parseAccount(who);
|
||||||
parseAccount(who).stream()
|
|
||||||
.map(a -> visibleto(args.userFactory.create(a)))
|
|
||||||
.collect(toImmutableList()));
|
|
||||||
} catch (QueryParseException e) {
|
} catch (QueryParseException e) {
|
||||||
if (e instanceof QueryRequiresAuthException) {
|
if (e instanceof QueryRequiresAuthException) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
// Otherwise continue: if it's not an account, maybe it's a group?
|
}
|
||||||
|
if (accounts != null) {
|
||||||
|
if (accounts.size() == 1) {
|
||||||
|
return visibleto(args.userFactory.create(Iterables.getOnlyElement(accounts)));
|
||||||
|
} else if (accounts.size() > 1) {
|
||||||
|
throw error(String.format("\"%s\" resolves to multiple accounts", who));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If its not an account, maybe its a group?
|
||||||
Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
|
Collection<GroupReference> suggestions = args.groupBackend.suggest(who, null);
|
||||||
if (!suggestions.isEmpty()) {
|
if (!suggestions.isEmpty()) {
|
||||||
HashSet<AccountGroup.UUID> ids = new HashSet<>();
|
HashSet<AccountGroup.UUID> ids = new HashSet<>();
|
||||||
|
|||||||
@@ -1784,16 +1784,43 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
|
|||||||
assertQuery(q + " visibleto:self", change2, change1);
|
assertQuery(q + " visibleto:self", change2, change1);
|
||||||
|
|
||||||
// Second user cannot see first user's private change
|
// Second user cannot see first user's private change
|
||||||
Account.Id user2 = createAccount("anotheruser");
|
Account.Id user2 = createAccount("user2");
|
||||||
assertQuery(q + " visibleto:" + user2.get(), change1);
|
assertQuery(q + " visibleto:" + user2.get(), change1);
|
||||||
assertQuery(q + " visibleto:anotheruser", change1);
|
assertQuery(q + " visibleto:user2", change1);
|
||||||
|
|
||||||
String g1 = createGroup("group1", "Administrators");
|
String g1 = createGroup("group1", "Administrators");
|
||||||
gApi.groups().id(g1).addMembers("anotheruser");
|
gApi.groups().id(g1).addMembers("user2");
|
||||||
assertQuery(q + " visibleto:" + g1, change1);
|
assertQuery(q + " visibleto:" + g1, change1);
|
||||||
|
|
||||||
requestContext.setContext(newRequestContext(user2));
|
requestContext.setContext(newRequestContext(user2));
|
||||||
assertQuery("is:visible", change1);
|
assertQuery("is:visible", change1);
|
||||||
|
|
||||||
|
Account.Id user3 = createAccount("user3");
|
||||||
|
|
||||||
|
// Explicitly authenticate user2 and user3 so that display name gets set
|
||||||
|
AuthRequest authRequest = AuthRequest.forUser("user2");
|
||||||
|
authRequest.setDisplayName("Another User");
|
||||||
|
authRequest.setEmailAddress("user2@example.com");
|
||||||
|
accountManager.authenticate(authRequest);
|
||||||
|
authRequest = AuthRequest.forUser("user3");
|
||||||
|
authRequest.setDisplayName("Another User");
|
||||||
|
authRequest.setEmailAddress("user3@example.com");
|
||||||
|
accountManager.authenticate(authRequest);
|
||||||
|
|
||||||
|
// Switch to user3
|
||||||
|
requestContext.setContext(newRequestContext(user3));
|
||||||
|
Change change3 = insert(repo, newChange(repo), user3);
|
||||||
|
Change change4 = insert(repo, newChangePrivate(repo), user3);
|
||||||
|
|
||||||
|
// User3 can see both their changes and the first user's change
|
||||||
|
assertQuery(q + " visibleto:" + user3.get(), change4, change3, change1);
|
||||||
|
|
||||||
|
// User2 cannot see user3's private change
|
||||||
|
assertQuery(q + " visibleto:" + user2.get(), change3, change1);
|
||||||
|
|
||||||
|
// Query as user3 by display name matching user2 and user3; bad request
|
||||||
|
assertFailingQuery(
|
||||||
|
q + " visibleto:\"Another User\"", "\"Another User\" resolves to multiple accounts");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -496,9 +496,7 @@ class GrChangeActions extends mixinBehaviors( [
|
|||||||
}
|
}
|
||||||
|
|
||||||
_getRebaseAction(revisionActions) {
|
_getRebaseAction(revisionActions) {
|
||||||
return this._getRevisionAction(revisionActions, 'rebase',
|
return this._getRevisionAction(revisionActions, 'rebase', null);
|
||||||
{rebaseOnCurrent: null}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_getRevisionAction(revisionActions, actionName, emptyActionValue) {
|
_getRevisionAction(revisionActions, actionName, emptyActionValue) {
|
||||||
@@ -523,7 +521,7 @@ class GrChangeActions extends mixinBehaviors( [
|
|||||||
.then(revisionActions => {
|
.then(revisionActions => {
|
||||||
if (!revisionActions) { return; }
|
if (!revisionActions) { return; }
|
||||||
|
|
||||||
this.revisionActions = this._updateRebaseAction(revisionActions);
|
this.revisionActions = revisionActions;
|
||||||
this._sendShowRevisionActions({
|
this._sendShowRevisionActions({
|
||||||
change: this.change,
|
change: this.change,
|
||||||
revisionActions,
|
revisionActions,
|
||||||
@@ -548,18 +546,6 @@ class GrChangeActions extends mixinBehaviors( [
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateRebaseAction(revisionActions) {
|
|
||||||
if (revisionActions && revisionActions.rebase) {
|
|
||||||
revisionActions.rebase.rebaseOnCurrent =
|
|
||||||
!!revisionActions.rebase.enabled;
|
|
||||||
this._parentIsCurrent = !revisionActions.rebase.enabled;
|
|
||||||
revisionActions.rebase.enabled = true;
|
|
||||||
} else {
|
|
||||||
this._parentIsCurrent = true;
|
|
||||||
}
|
|
||||||
return revisionActions;
|
|
||||||
}
|
|
||||||
|
|
||||||
_changeChanged() {
|
_changeChanged() {
|
||||||
this.reload();
|
this.reload();
|
||||||
}
|
}
|
||||||
@@ -1121,8 +1107,9 @@ class GrChangeActions extends mixinBehaviors( [
|
|||||||
}
|
}
|
||||||
|
|
||||||
_calculateDisabled(action, hasKnownChainState) {
|
_calculateDisabled(action, hasKnownChainState) {
|
||||||
if (action.__key === 'rebase' && hasKnownChainState === false) {
|
if (action.__key === 'rebase') {
|
||||||
return true;
|
// Rebase button is only disabled when change has no parent(s).
|
||||||
|
return hasKnownChainState === false;
|
||||||
}
|
}
|
||||||
return !action.enabled;
|
return !action.enabled;
|
||||||
}
|
}
|
||||||
@@ -1602,6 +1589,13 @@ class GrChangeActions extends mixinBehaviors( [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_computeRebaseOnCurrent(revisionRebaseAction) {
|
||||||
|
if (revisionRebaseAction) {
|
||||||
|
return !!revisionRebaseAction.enabled;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Occasionally, a change created by a change action is not yet knwon to the
|
* Occasionally, a change created by a change action is not yet knwon to the
|
||||||
* API for a brief time. Wait for the given change number to be recognized.
|
* API for a brief time. Wait for the given change number to be recognized.
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export const htmlTemplate = html`
|
|||||||
</gr-dropdown>
|
</gr-dropdown>
|
||||||
</div>
|
</div>
|
||||||
<gr-overlay id="overlay" with-backdrop="">
|
<gr-overlay id="overlay" with-backdrop="">
|
||||||
<gr-confirm-rebase-dialog id="confirmRebase" class="confirmDialog" change-number="[[change._number]]" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" branch="[[change.branch]]" has-parent="[[hasParent]]" rebase-on-current="[[_revisionRebaseAction.rebaseOnCurrent]]" hidden=""></gr-confirm-rebase-dialog>
|
<gr-confirm-rebase-dialog id="confirmRebase" class="confirmDialog" change-number="[[change._number]]" on-confirm="_handleRebaseConfirm" on-cancel="_handleConfirmDialogCancel" branch="[[change.branch]]" has-parent="[[hasParent]]" rebase-on-current="[[_computeRebaseOnCurrent(_revisionRebaseAction)]]" hidden=""></gr-confirm-rebase-dialog>
|
||||||
<gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" change-status="[[changeStatus]]" commit-message="[[commitMessage]]" commit-num="[[commitNum]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-cherrypick-dialog>
|
<gr-confirm-cherrypick-dialog id="confirmCherrypick" class="confirmDialog" change-status="[[changeStatus]]" commit-message="[[commitMessage]]" commit-num="[[commitNum]]" on-confirm="_handleCherrypickConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-cherrypick-dialog>
|
||||||
<gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-cherrypick-conflict-dialog>
|
<gr-confirm-cherrypick-conflict-dialog id="confirmCherrypickConflict" class="confirmDialog" on-confirm="_handleCherrypickConflictConfirm" on-cancel="_handleConfirmDialogCancel" hidden=""></gr-confirm-cherrypick-conflict-dialog>
|
||||||
<gr-confirm-move-dialog id="confirmMove" class="confirmDialog" on-confirm="_handleMoveConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-move-dialog>
|
<gr-confirm-move-dialog id="confirmMove" class="confirmDialog" on-confirm="_handleMoveConfirm" on-cancel="_handleConfirmDialogCancel" project="[[change.project]]" hidden=""></gr-confirm-move-dialog>
|
||||||
|
|||||||
@@ -393,7 +393,7 @@ suite('gr-change-actions tests', () => {
|
|||||||
|
|
||||||
action.enabled = false;
|
action.enabled = false;
|
||||||
assert.equal(
|
assert.equal(
|
||||||
element._calculateDisabled(action, hasKnownChainState), true);
|
element._calculateDisabled(action, hasKnownChainState), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('rebase change', done => {
|
test('rebase change', done => {
|
||||||
@@ -416,7 +416,6 @@ suite('gr-change-actions tests', () => {
|
|||||||
};
|
};
|
||||||
assert.isTrue(fetchChangesStub.called);
|
assert.isTrue(fetchChangesStub.called);
|
||||||
element._handleRebaseConfirm({detail: {base: '1234'}});
|
element._handleRebaseConfirm({detail: {base: '1234'}});
|
||||||
rebaseAction.rebaseOnCurrent = true;
|
|
||||||
assert.deepEqual(fireActionStub.lastCall.args,
|
assert.deepEqual(fireActionStub.lastCall.args,
|
||||||
['/rebase', rebaseAction, true, {base: '1234'}]);
|
['/rebase', rebaseAction, true, {base: '1234'}]);
|
||||||
done();
|
done();
|
||||||
@@ -1924,57 +1923,23 @@ suite('gr-change-actions tests', () => {
|
|||||||
assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
|
assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
|
test('_computeRebaseOnCurrent', () => {
|
||||||
const currentRevisionActions = {
|
const rebaseAction = {
|
||||||
cherrypick: {
|
|
||||||
enabled: true,
|
|
||||||
label: 'Cherry Pick',
|
|
||||||
method: 'POST',
|
|
||||||
title: 'cherrypick',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
element._parentIsCurrent = undefined;
|
|
||||||
element._updateRebaseAction(currentRevisionActions);
|
|
||||||
assert.isTrue(element._parentIsCurrent);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('_updateRebaseAction', () => {
|
|
||||||
const currentRevisionActions = {
|
|
||||||
cherrypick: {
|
|
||||||
enabled: true,
|
|
||||||
label: 'Cherry Pick',
|
|
||||||
method: 'POST',
|
|
||||||
title: 'cherrypick',
|
|
||||||
},
|
|
||||||
rebase: {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
label: 'Rebase',
|
label: 'Rebase',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
title: 'Rebase onto tip of branch or parent change',
|
title: 'Rebase onto tip of branch or parent change',
|
||||||
},
|
|
||||||
};
|
};
|
||||||
element._parentIsCurrent = undefined;
|
|
||||||
|
|
||||||
// Rebase enabled should always end up true.
|
|
||||||
// When rebase is enabled initially, rebaseOnCurrent should be set to
|
// When rebase is enabled initially, rebaseOnCurrent should be set to
|
||||||
// true.
|
// true.
|
||||||
assert.equal(element._updateRebaseAction(currentRevisionActions),
|
assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
|
||||||
currentRevisionActions);
|
|
||||||
|
|
||||||
assert.isTrue(currentRevisionActions.rebase.enabled);
|
delete rebaseAction.enabled;
|
||||||
assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
|
|
||||||
assert.isFalse(element._parentIsCurrent);
|
|
||||||
|
|
||||||
delete currentRevisionActions.rebase.enabled;
|
|
||||||
|
|
||||||
// When rebase is not enabled initially, rebaseOnCurrent should be set to
|
// When rebase is not enabled initially, rebaseOnCurrent should be set to
|
||||||
// false.
|
// false.
|
||||||
assert.equal(element._updateRebaseAction(currentRevisionActions),
|
assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
|
||||||
currentRevisionActions);
|
|
||||||
|
|
||||||
assert.isTrue(currentRevisionActions.rebase.enabled);
|
|
||||||
assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
|
|
||||||
assert.isTrue(element._parentIsCurrent);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -292,7 +292,6 @@ class GrChangeView extends mixinBehaviors( [
|
|||||||
_loading: Boolean,
|
_loading: Boolean,
|
||||||
/** @type {?} */
|
/** @type {?} */
|
||||||
_projectConfig: Object,
|
_projectConfig: Object,
|
||||||
_rebaseOnCurrent: Boolean,
|
|
||||||
_replyButtonLabel: {
|
_replyButtonLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
value: 'Reply',
|
value: 'Reply',
|
||||||
|
|||||||
@@ -1218,15 +1218,7 @@ class GrRestApiInterface extends mixinBehaviors( [
|
|||||||
patchNum,
|
patchNum,
|
||||||
reportEndpointAsIs: true,
|
reportEndpointAsIs: true,
|
||||||
};
|
};
|
||||||
return this._getChangeURLAndFetch(req).then(revisionActions => {
|
return this._getChangeURLAndFetch(req);
|
||||||
// The rebase button on change screen is always enabled.
|
|
||||||
if (revisionActions.rebase) {
|
|
||||||
revisionActions.rebase.rebaseOnCurrent =
|
|
||||||
!!revisionActions.rebase.enabled;
|
|
||||||
revisionActions.rebase.enabled = true;
|
|
||||||
}
|
|
||||||
return revisionActions;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -332,36 +332,6 @@ suite('gr-rest-api-interface tests', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
suite('rebase action', () => {
|
|
||||||
let resolve_fetchJSON;
|
|
||||||
setup(() => {
|
|
||||||
sandbox.stub(element._restApiHelper, 'fetchJSON').returns(
|
|
||||||
new Promise(resolve => {
|
|
||||||
resolve_fetchJSON = resolve;
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('no rebase on current', done => {
|
|
||||||
element.getChangeRevisionActions('42', '1337').then(
|
|
||||||
response => {
|
|
||||||
assert.isTrue(response.rebase.enabled);
|
|
||||||
assert.isFalse(response.rebase.rebaseOnCurrent);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
resolve_fetchJSON({rebase: {}});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('rebase on current', done => {
|
|
||||||
element.getChangeRevisionActions('42', '1337').then(
|
|
||||||
response => {
|
|
||||||
assert.isTrue(response.rebase.enabled);
|
|
||||||
assert.isTrue(response.rebase.rebaseOnCurrent);
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
resolve_fetchJSON({rebase: {enabled: true}});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('server error', done => {
|
test('server error', done => {
|
||||||
const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
|
const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
|
||||||
window.fetch.returns(Promise.resolve({ok: false}));
|
window.fetch.returns(Promise.resolve({ok: false}));
|
||||||
|
|||||||
Reference in New Issue
Block a user