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:
 | 
			
		||||
= 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.
 | 
			
		||||
 | 
			
		||||
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?
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
link:https://gatling.io/docs/current/http/http_protocol/[here,role=external,window=_blank].
 | 
			
		||||
 | 
			
		||||
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]
 | 
			
		||||
to run tests at Git protocol level.
 | 
			
		||||
However, in the scenarios that were initially proposed, the
 | 
			
		||||
link:https://github.com/GerritForge/gatling-git[Gatling Git extension,role=external,window=_blank] was
 | 
			
		||||
leveraged to run tests at the Git protocol level.
 | 
			
		||||
 | 
			
		||||
Gatling is written in Scala, but the abstraction provided by the Gatling DSL makes the scenarios
 | 
			
		||||
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]
 | 
			
		||||
blog post has more introductory information.
 | 
			
		||||
 | 
			
		||||
Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory
 | 
			
		||||
should be formatted using the mainstream
 | 
			
		||||
Examples of scenarios can be found in the `e2e-tests` directory. The files in that directory should
 | 
			
		||||
be formatted using the mainstream
 | 
			
		||||
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.
 | 
			
		||||
 | 
			
		||||
@@ -86,10 +95,11 @@ ssh-keyscan -t rsa -p 29418 localhost > ~/.ssh/known_hosts
 | 
			
		||||
=== Input file
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
repository used during the load test. That file currently looks like below. This scenario serves
 | 
			
		||||
as a simple example with no actual load in it. It can be used to test or validate the local setup.
 | 
			
		||||
More complex scenarios can be further developed, under the `com.google.gerrit.scenarios` package.
 | 
			
		||||
`src/test/resources/data/com/google/gerrit/scenarios/CloneUsingBothProtocols.json` file. Such a
 | 
			
		||||
file contains the commands and repository used during the e2e test. That file currently looks like
 | 
			
		||||
below. This scenario serves as a simple example with no actual load in it. It can be used to test
 | 
			
		||||
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`
 | 
			
		||||
* `push`
 | 
			
		||||
 | 
			
		||||
=== Project and HTTP credentials
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
`src/test/resources/application.conf`, depending on the above commands used.
 | 
			
		||||
`CloneUsingBothProtocols` scenario already includes creating that project and deleting it once done
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@@ -142,6 +162,27 @@ Gatling's logging level.
 | 
			
		||||
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
 | 
			
		||||
------
 | 
			
		||||
Part of link:index.html[Gerrit Code Review]
 | 
			
		||||
 
 | 
			
		||||
@@ -65,8 +65,8 @@ see <<dev-bazel#tests,Running Unit Tests with Bazel>>.
 | 
			
		||||
[[e2e]]
 | 
			
		||||
=== End-to-end tests
 | 
			
		||||
 | 
			
		||||
<<dev-e2e-tests#,This document>> describes how load test scenarios are
 | 
			
		||||
implemented using link:https://gatling.io/[`Gatling`].
 | 
			
		||||
<<dev-e2e-tests#,This document>> describes how `e2e` (load or functional) test
 | 
			
		||||
scenarios are implemented using link:https://gatling.io/[`Gatling`,role=external,window=_blank].
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
== 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
 | 
			
		||||
 | 
			
		||||
import io.gatling.core.Predef._
 | 
			
		||||
import io.gatling.core.feeder.FileBasedFeederBuilder
 | 
			
		||||
import io.gatling.core.structure.ScenarioBuilder
 | 
			
		||||
 | 
			
		||||
import scala.concurrent.duration._
 | 
			
		||||
 | 
			
		||||
class CloneUsingBothProtocols extends GitSimulation {
 | 
			
		||||
  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).queue
 | 
			
		||||
 | 
			
		||||
  private val test: ScenarioBuilder = scenario(name)
 | 
			
		||||
      .feed(data)
 | 
			
		||||
      .exec(request)
 | 
			
		||||
      .exec(gitRequest)
 | 
			
		||||
 | 
			
		||||
  private val createProject = new CreateProject
 | 
			
		||||
  private val deleteProject = new DeleteProject
 | 
			
		||||
 | 
			
		||||
  setUp(
 | 
			
		||||
    createProject.test.inject(
 | 
			
		||||
      atOnceUsers(1)
 | 
			
		||||
    ),
 | 
			
		||||
    test.inject(
 | 
			
		||||
      nothingFor(1 second),
 | 
			
		||||
      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 com.github.barbasa.gatling.git.GitRequestSession
 | 
			
		||||
import com.github.barbasa.gatling.git.protocol.GitProtocol
 | 
			
		||||
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.feeder.FileBasedFeederBuilder
 | 
			
		||||
import org.apache.commons.io.FileUtils
 | 
			
		||||
import org.eclipse.jgit.hooks.CommitMsgHook
 | 
			
		||||
 | 
			
		||||
class GitSimulation extends Simulation {
 | 
			
		||||
 | 
			
		||||
  implicit val conf: GatlingGitConfiguration = GatlingGitConfiguration()
 | 
			
		||||
class GitSimulation extends GerritSimulation {
 | 
			
		||||
  implicit val postMessageHook: Option[String] = Some(s"hooks/${CommitMsgHook.NAME}")
 | 
			
		||||
 | 
			
		||||
  protected val name: String = this.getClass.getSimpleName
 | 
			
		||||
  protected val data: FileBasedFeederBuilder[Any]#F = jsonFile(s"data/$name.json").circular
 | 
			
		||||
  protected val request = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
 | 
			
		||||
  protected val protocol: GitProtocol = GitProtocol()
 | 
			
		||||
  protected val gitRequest = new GitRequestBuilder(GitRequestSession("${cmd}", "${url}"))
 | 
			
		||||
  protected val gitProtocol: GitProtocol = GitProtocol()
 | 
			
		||||
 | 
			
		||||
  after {
 | 
			
		||||
    Thread.sleep(5000)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,18 @@
 | 
			
		||||
package com.google.gerrit.scenarios
 | 
			
		||||
 | 
			
		||||
import io.gatling.core.Predef._
 | 
			
		||||
import io.gatling.core.feeder.FileBasedFeederBuilder
 | 
			
		||||
import io.gatling.core.structure.ScenarioBuilder
 | 
			
		||||
 | 
			
		||||
import scala.concurrent.duration._
 | 
			
		||||
 | 
			
		||||
class ReplayRecordsFromFeeder extends GitSimulation {
 | 
			
		||||
  private val data: FileBasedFeederBuilder[Any]#F = jsonFile(resource).circular
 | 
			
		||||
 | 
			
		||||
  private val test: ScenarioBuilder = scenario(name)
 | 
			
		||||
      .repeat(10000) {
 | 
			
		||||
        feed(data)
 | 
			
		||||
            .exec(request)
 | 
			
		||||
            .exec(gitRequest)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  setUp(
 | 
			
		||||
@@ -34,6 +36,6 @@ class ReplayRecordsFromFeeder extends GitSimulation {
 | 
			
		||||
      rampUsers(10) during (5 seconds),
 | 
			
		||||
      constantUsersPerSec(20) during (15 seconds),
 | 
			
		||||
      constantUsersPerSec(20) during (15 seconds) randomized
 | 
			
		||||
    )).protocols(protocol)
 | 
			
		||||
    )).protocols(gitProtocol)
 | 
			
		||||
      .maxDuration(60 seconds)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@
 | 
			
		||||
 | 
			
		||||
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.server.account.AccountResolver.isSelf;
 | 
			
		||||
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.base.Enums;
 | 
			
		||||
import com.google.common.base.Splitter;
 | 
			
		||||
import com.google.common.collect.Iterables;
 | 
			
		||||
import com.google.common.collect.Lists;
 | 
			
		||||
import com.google.common.flogger.FluentLogger;
 | 
			
		||||
import com.google.common.primitives.Ints;
 | 
			
		||||
@@ -985,18 +985,23 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData, ChangeQueryBuil
 | 
			
		||||
    if (isSelf(who)) {
 | 
			
		||||
      return isVisible();
 | 
			
		||||
    }
 | 
			
		||||
    Set<Account.Id> accounts = null;
 | 
			
		||||
    try {
 | 
			
		||||
      return Predicate.or(
 | 
			
		||||
          parseAccount(who).stream()
 | 
			
		||||
              .map(a -> visibleto(args.userFactory.create(a)))
 | 
			
		||||
              .collect(toImmutableList()));
 | 
			
		||||
      accounts = parseAccount(who);
 | 
			
		||||
    } catch (QueryParseException e) {
 | 
			
		||||
      if (e instanceof QueryRequiresAuthException) {
 | 
			
		||||
        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);
 | 
			
		||||
    if (!suggestions.isEmpty()) {
 | 
			
		||||
      HashSet<AccountGroup.UUID> ids = new HashSet<>();
 | 
			
		||||
 
 | 
			
		||||
@@ -1784,16 +1784,43 @@ public abstract class AbstractQueryChangesTest extends GerritServerTests {
 | 
			
		||||
    assertQuery(q + " visibleto:self", change2, change1);
 | 
			
		||||
 | 
			
		||||
    // 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:anotheruser", change1);
 | 
			
		||||
    assertQuery(q + " visibleto:user2", change1);
 | 
			
		||||
 | 
			
		||||
    String g1 = createGroup("group1", "Administrators");
 | 
			
		||||
    gApi.groups().id(g1).addMembers("anotheruser");
 | 
			
		||||
    gApi.groups().id(g1).addMembers("user2");
 | 
			
		||||
    assertQuery(q + " visibleto:" + g1, change1);
 | 
			
		||||
 | 
			
		||||
    requestContext.setContext(newRequestContext(user2));
 | 
			
		||||
    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
 | 
			
		||||
 
 | 
			
		||||
@@ -496,9 +496,7 @@ class GrChangeActions extends mixinBehaviors( [
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getRebaseAction(revisionActions) {
 | 
			
		||||
    return this._getRevisionAction(revisionActions, 'rebase',
 | 
			
		||||
        {rebaseOnCurrent: null}
 | 
			
		||||
    );
 | 
			
		||||
    return this._getRevisionAction(revisionActions, 'rebase', null);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _getRevisionAction(revisionActions, actionName, emptyActionValue) {
 | 
			
		||||
@@ -523,7 +521,7 @@ class GrChangeActions extends mixinBehaviors( [
 | 
			
		||||
        .then(revisionActions => {
 | 
			
		||||
          if (!revisionActions) { return; }
 | 
			
		||||
 | 
			
		||||
          this.revisionActions = this._updateRebaseAction(revisionActions);
 | 
			
		||||
          this.revisionActions = revisionActions;
 | 
			
		||||
          this._sendShowRevisionActions({
 | 
			
		||||
            change: this.change,
 | 
			
		||||
            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() {
 | 
			
		||||
    this.reload();
 | 
			
		||||
  }
 | 
			
		||||
@@ -1121,8 +1107,9 @@ class GrChangeActions extends mixinBehaviors( [
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _calculateDisabled(action, hasKnownChainState) {
 | 
			
		||||
    if (action.__key === 'rebase' && hasKnownChainState === false) {
 | 
			
		||||
      return true;
 | 
			
		||||
    if (action.__key === 'rebase') {
 | 
			
		||||
      // Rebase button is only disabled when change has no parent(s).
 | 
			
		||||
      return hasKnownChainState === false;
 | 
			
		||||
    }
 | 
			
		||||
    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
 | 
			
		||||
   * API for a brief time. Wait for the given change number to be recognized.
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,7 @@ export const htmlTemplate = html`
 | 
			
		||||
        </gr-dropdown>
 | 
			
		||||
    </div>
 | 
			
		||||
    <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-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>
 | 
			
		||||
 
 | 
			
		||||
@@ -393,7 +393,7 @@ suite('gr-change-actions tests', () => {
 | 
			
		||||
 | 
			
		||||
      action.enabled = false;
 | 
			
		||||
      assert.equal(
 | 
			
		||||
          element._calculateDisabled(action, hasKnownChainState), true);
 | 
			
		||||
          element._calculateDisabled(action, hasKnownChainState), false);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('rebase change', done => {
 | 
			
		||||
@@ -416,7 +416,6 @@ suite('gr-change-actions tests', () => {
 | 
			
		||||
        };
 | 
			
		||||
        assert.isTrue(fetchChangesStub.called);
 | 
			
		||||
        element._handleRebaseConfirm({detail: {base: '1234'}});
 | 
			
		||||
        rebaseAction.rebaseOnCurrent = true;
 | 
			
		||||
        assert.deepEqual(fireActionStub.lastCall.args,
 | 
			
		||||
            ['/rebase', rebaseAction, true, {base: '1234'}]);
 | 
			
		||||
        done();
 | 
			
		||||
@@ -1924,57 +1923,23 @@ suite('gr-change-actions tests', () => {
 | 
			
		||||
      assert.strictEqual(element.$.confirmRebase.rebaseOnCurrent, null);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    test('_updateRebaseAction sets _parentIsCurrent on no rebase', () => {
 | 
			
		||||
      const currentRevisionActions = {
 | 
			
		||||
        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: {
 | 
			
		||||
    test('_computeRebaseOnCurrent', () => {
 | 
			
		||||
      const rebaseAction = {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        label: 'Rebase',
 | 
			
		||||
        method: 'POST',
 | 
			
		||||
        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
 | 
			
		||||
      // true.
 | 
			
		||||
      assert.equal(element._updateRebaseAction(currentRevisionActions),
 | 
			
		||||
          currentRevisionActions);
 | 
			
		||||
      assert.isTrue(element._computeRebaseOnCurrent(rebaseAction));
 | 
			
		||||
 | 
			
		||||
      assert.isTrue(currentRevisionActions.rebase.enabled);
 | 
			
		||||
      assert.isTrue(currentRevisionActions.rebase.rebaseOnCurrent);
 | 
			
		||||
      assert.isFalse(element._parentIsCurrent);
 | 
			
		||||
 | 
			
		||||
      delete currentRevisionActions.rebase.enabled;
 | 
			
		||||
      delete rebaseAction.enabled;
 | 
			
		||||
 | 
			
		||||
      // When rebase is not enabled initially, rebaseOnCurrent should be set to
 | 
			
		||||
      // false.
 | 
			
		||||
      assert.equal(element._updateRebaseAction(currentRevisionActions),
 | 
			
		||||
          currentRevisionActions);
 | 
			
		||||
 | 
			
		||||
      assert.isTrue(currentRevisionActions.rebase.enabled);
 | 
			
		||||
      assert.isFalse(currentRevisionActions.rebase.rebaseOnCurrent);
 | 
			
		||||
      assert.isTrue(element._parentIsCurrent);
 | 
			
		||||
      assert.isFalse(element._computeRebaseOnCurrent(rebaseAction));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -292,7 +292,6 @@ class GrChangeView extends mixinBehaviors( [
 | 
			
		||||
      _loading: Boolean,
 | 
			
		||||
      /** @type {?} */
 | 
			
		||||
      _projectConfig: Object,
 | 
			
		||||
      _rebaseOnCurrent: Boolean,
 | 
			
		||||
      _replyButtonLabel: {
 | 
			
		||||
        type: String,
 | 
			
		||||
        value: 'Reply',
 | 
			
		||||
 
 | 
			
		||||
@@ -1218,15 +1218,7 @@ class GrRestApiInterface extends mixinBehaviors( [
 | 
			
		||||
      patchNum,
 | 
			
		||||
      reportEndpointAsIs: true,
 | 
			
		||||
    };
 | 
			
		||||
    return this._getChangeURLAndFetch(req).then(revisionActions => {
 | 
			
		||||
      // The rebase button on change screen is always enabled.
 | 
			
		||||
      if (revisionActions.rebase) {
 | 
			
		||||
        revisionActions.rebase.rebaseOnCurrent =
 | 
			
		||||
            !!revisionActions.rebase.enabled;
 | 
			
		||||
        revisionActions.rebase.enabled = true;
 | 
			
		||||
      }
 | 
			
		||||
      return revisionActions;
 | 
			
		||||
    });
 | 
			
		||||
    return this._getChangeURLAndFetch(req);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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 => {
 | 
			
		||||
    const getResponseObjectStub = sandbox.stub(element, 'getResponseObject');
 | 
			
		||||
    window.fetch.returns(Promise.resolve({ok: false}));
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user