From 251255009a857656abf0fe7776b5ae4d68eb4cf7 Mon Sep 17 00:00:00 2001 From: kezhenxu94 Date: Wed, 20 Oct 2021 21:14:26 +0800 Subject: [PATCH] Add end-to-end test framework and some basic cases (#6419) --- .github/workflows/e2e.yml | 68 +++---- docker/build/hooks/build | 42 +--- dolphinscheduler-e2e/README.md | 98 +++++++++ .../dolphinscheduler-e2e-case/pom.xml | 40 ++++ .../e2e/cases/security/TenantE2ETest.java | 98 +++++++++ .../dolphinscheduler/e2e/pages/LoginPage.java | 47 +++++ .../e2e/pages/TenantPage.java | 78 ++++++++ .../docker/tenant/docker-compose.yaml | 35 ++++ .../dolphinscheduler-e2e-core/pom.xml | 32 +++ .../e2e/core/DolphinScheduler.java | 41 ++++ .../e2e/core/DolphinSchedulerExtension.java | 187 ++++++++++++++++++ .../e2e/core/TestDescription.java | 55 ++++++ .../src/main/resources/log4j2.xml | 31 +++ dolphinscheduler-e2e/lombok.config | 20 ++ dolphinscheduler-e2e/pom.xml | 138 +++++++++++++ .../server/StandaloneServer.java | 2 + .../pages/tenement/_source/createTenement.vue | 5 + .../security/pages/tenement/_source/list.vue | 4 +- .../pages/security/pages/tenement/index.vue | 2 +- dolphinscheduler-ui/src/js/conf/login/App.vue | 4 +- .../js/module/components/popup/popover.vue | 12 +- 21 files changed, 963 insertions(+), 76 deletions(-) create mode 100644 dolphinscheduler-e2e/README.md create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java create mode 100644 dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml create mode 100644 dolphinscheduler-e2e/lombok.config create mode 100644 dolphinscheduler-e2e/pom.xml diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 2fbbffa8bd..e707259ab9 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -15,60 +15,54 @@ # limitations under the License. # -on: ["pull_request"] +on: + pull_request: + push: + branches: + - e2e + env: - DOCKER_DIR: ./docker - LOG_DIR: /tmp/dolphinscheduler + TAG: ci + RECORDING_PATH: /tmp/recording -name: Test +name: E2E concurrency: group: e2e-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - test: - name: E2E + e2e: + name: ${{ matrix.case.name }} runs-on: ubuntu-latest + strategy: + matrix: + case: + - name: Tenant + class: org.apache.dolphinscheduler.e2e.cases.security.TenantE2ETest steps: - uses: actions/checkout@v2 with: submodules: true - name: Sanity Check uses: ./.github/actions/sanity-check - - uses: actions/cache@v1 + - name: Cache local Maven repository + uses: actions/cache@v2 with: path: ~/.m2/repository - key: ${{ runner.os }}-maven + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-maven- - name: Build Image + run: TAG=ci sh ./docker/build/hooks/build + - name: Run Test run: | - sh ./docker/build/hooks/build - - name: Docker Run - run: | - export VERSION=$(cat $(pwd)/pom.xml | grep '' -m 1 | awk '{print $1}' | sed 's///' | sed 's/<\/revision>//') - sed -i "s/apache\/dolphinscheduler:latest/apache\/dolphinscheduler:${VERSION}/g" $(pwd)/docker/docker-swarm/docker-compose.yml - docker-compose -f $(pwd)/docker/docker-swarm/docker-compose.yml up -d - - name: Check Server Status - run: sh $(pwd)/docker/docker-swarm/check - - name: Prepare e2e env - run: | - sudo apt-get install -y libxss1 libappindicator1 libindicator7 xvfb unzip libgbm1 - wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb - sudo dpkg -i google-chrome*.deb - sudo apt-get install -f -y - google-chrome -version - googleVersion=$(curl -s https://chromedriver.storage.googleapis.com/LATEST_RELEASE) - wget -N https://chromedriver.storage.googleapis.com/${googleVersion}/chromedriver_linux64.zip - unzip chromedriver_linux64.zip - sudo mv -f chromedriver /usr/local/share/chromedriver - sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver -# - name: Run e2e Test -# run: cd ./e2e && mvn -B clean test - - name: Collect logs - if: failure() - uses: actions/upload-artifact@v2 + ./mvnw -f dolphinscheduler-e2e/pom.xml -am \ + -DfailIfNoTests=false \ + -Dtest=${{ matrix.case.class }} test + - uses: actions/upload-artifact@v2 + if: always() + name: Upload Recording with: - name: dslogs - path: ${{ github.workspace }}/docker/docker-swarm/dolphinscheduler-logs - - + name: recording + path: ${{ env.RECORDING_PATH }} + retention-days: 1 diff --git a/docker/build/hooks/build b/docker/build/hooks/build index 70ea260dea..1e6a96549b 100755 --- a/docker/build/hooks/build +++ b/docker/build/hooks/build @@ -18,41 +18,17 @@ set -e -echo "------ dolphinscheduler start - build -------" -printenv +ROOT_DIR=$(dirname "$0")/../../.. +MVN="$ROOT_DIR"/mvnw +VERSION=$("$MVN" -q -DforceStdout -N org.apache.maven.plugins:maven-help-plugin:3.2.0:evaluate -Dexpression=project.version) -if [ -z "${VERSION}" ] -then - echo "set default environment variable [VERSION]" - export VERSION=$(cat $(pwd)/pom.xml | grep '' -m 1 | awk '{print $1}' | sed 's///' | sed 's/<\/version>//') -fi +DOCKER_REPO=${DOCKER_REPO:-"apache/dolphinscheduler"} +TAG=${TAG:-"$VERSION"} -if [ "${DOCKER_REPO}x" = "x" ] -then - echo "set default environment variable [DOCKER_REPO]" - export DOCKER_REPO='apache/dolphinscheduler' -fi +echo "Building Docker image as: $DOCKER_REPO:$TAG" -echo "Version: $VERSION" -echo "Repo: $DOCKER_REPO" +"$MVN" -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 -echo -e "Current Directory is $(pwd)\n" +cp "$ROOT_DIR"/dolphinscheduler-dist/target/apache-dolphinscheduler-$VERSION-bin.tar.gz "$ROOT_DIR"/docker/build/ -# maven package(Project Directory) -echo -e "./mvnw -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120" -./mvnw -B clean package -Prelease -Dmaven.test.skip=true -Dhttp.keepAlive=false -Dmaven.wagon.http.pool=false -Dmaven.wagon.httpconnectionManager.ttlSeconds=120 - -# mv dolphinscheduler-bin.tar.gz file to docker/build directory -echo -e "mv $(pwd)/dolphinscheduler-dist/target/apache-dolphinscheduler-${VERSION}-bin.tar.gz $(pwd)/docker/build/\n" -mv $(pwd)/dolphinscheduler-dist/target/apache-dolphinscheduler-${VERSION}-bin.tar.gz $(pwd)/docker/build/ - -# docker build -BUILD_COMMAND="docker build --build-arg VERSION=${VERSION} -t $DOCKER_REPO:${VERSION} $(pwd)/docker/build/" -echo -e "$BUILD_COMMAND\n" -if (docker info 2> /dev/null | grep -i "ERROR"); then - sudo $BUILD_COMMAND -else - $BUILD_COMMAND -fi - -echo "------ dolphinscheduler end - build -------" +docker build --build-arg VERSION=$VERSION -t $DOCKER_REPO:$TAG "$ROOT_DIR"/docker/build/ diff --git a/dolphinscheduler-e2e/README.md b/dolphinscheduler-e2e/README.md new file mode 100644 index 0000000000..35c3043a1c --- /dev/null +++ b/dolphinscheduler-e2e/README.md @@ -0,0 +1,98 @@ +# DolphinScheduler End-to-End Test + +## Page Object Model + +DolphinScheduler End-to-End test respects +the [Page Object Model (POM)](https://www.selenium.dev/documentation/guidelines/page_object_models/) design pattern. +Every page of DolphinScheduler is abstracted into a class for better maintainability. + +### Example + +The login page is abstracted +as [`LoginPage`](dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java), with the +following fields, + +```java +public final class LoginPage { + @FindBy(id = "input-username") + private WebElement inputUsername; + + @FindBy(id = "input-password") + private WebElement inputPassword; + + @FindBy(id = "button-login") + private WebElement buttonLogin; +} +``` + +where `inputUsername`, `inputPassword` and `buttonLogin` are the main elements on UI that we are interested in. They are +annotated with `FindBy` so that the test framework knows how to locate the elements on UI. You can locate the elements +by `id`, `className`, `css` selector, `tagName`, or even `xpath`, please refer +to [the JavaDoc](https://www.selenium.dev/selenium/docs/api/java/org/openqa/selenium/support/FindBy.html). + +**Note:** for better maintainability, it's essential to add some convenient `id` or `class` on UI for the wanted +elements if needed, avoid using too complex `xpath` selector or `css` selector that is not maintainable when UI have +styles changes. + +With those fields declared, we should also initialize them with a web driver. Here we pass the web driver into the +constructor and invoke `PageFactory.initElements` to initialize those fields, + +```java +public final class LoginPage { + // ... + public LoginPage(RemoteWebDriver driver) { + this.driver = driver; + + PageFactory.initElements(driver, this); + } +} +``` + +then, all those UI elements are properly filled in. + +## Test Environment Setup + +DolphinScheduler End-to-End test uses [testcontainers](https://www.testcontainers.org) to set up the testing +environment, with docker compose. + +Typically, every test case needs one or more `docker-compose.yaml` files to set up all needed components, and expose the +DolphinScheduler UI port for testing. You can use `@DolphinScheduler(composeFiles = "")` and pass +the `docker-compose.yaml` files to automatically set up the environment in the test class. + +```java + +@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml") +class TenantE2ETest { +} +``` + +You can get the web driver that is ready for testing in the class by adding a field of type `RemoteWebDriver`, which +will be automatically injected via the testing framework. + +```java + +@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml") +class TenantE2ETest { + private RemoteWebDriver browser; +} +``` + +Then the field `browser` can be used in the test methods. + +```java + +@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml") +class TenantE2ETest { + private RemoteWebDriver browser; + + @Test + void testLogin() { + final LoginPage page = new LoginPage(browser); // <<-- use the browser injected + } +} +``` + +## Notes + +- For UI tests, it's common that the pages might need some time to load, or the operations might need some time to + complete, we can use `await().untilAsserted(() -> {})` to wait for the assertions. diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml new file mode 100644 index 0000000000..17e63bec70 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/pom.xml @@ -0,0 +1,40 @@ + + + + + + dolphinscheduler-e2e + org.apache.dolphinscheduler + 1.0-SNAPSHOT + + 4.0.0 + + dolphinscheduler-e2e-case + + + + org.apache.dolphinscheduler + dolphinscheduler-e2e-core + ${project.version} + + + diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java new file mode 100644 index 0000000000..ab3159a275 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/cases/security/TenantE2ETest.java @@ -0,0 +1,98 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.cases.security; + + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +import org.apache.dolphinscheduler.e2e.core.DolphinScheduler; +import org.apache.dolphinscheduler.e2e.pages.LoginPage; +import org.apache.dolphinscheduler.e2e.pages.TenantPage; + +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebDriver; + +@DolphinScheduler(composeFiles = "docker/tenant/docker-compose.yaml") +class TenantE2ETest { + private RemoteWebDriver browser; + + @Test + @Order(1) + void testLogin() { + final LoginPage page = new LoginPage(browser); + page.inputUsername().sendKeys("admin"); + page.inputPassword().sendKeys("dolphinscheduler123"); + page.buttonLogin().click(); + } + + @Test + @Order(10) + void testCreateTenant() { + final TenantPage page = new TenantPage(browser); + final String tenant = System.getProperty("user.name"); + + page.buttonCreateTenant().click(); + page.createTenantForm().inputTenantCode().sendKeys(tenant); + page.createTenantForm().inputDescription().sendKeys("Test"); + page.createTenantForm().buttonSubmit().click(); + + await().untilAsserted(() -> assertThat(page.tenantList()) + .as("Tenant list should contain newly-created tenant") + .extracting(WebElement::getText) + .anyMatch(it -> it.contains(tenant))); + } + + @Test + @Order(20) + void testCreateDuplicateTenant() { + final String tenant = System.getProperty("user.name"); + final TenantPage page = new TenantPage(browser); + page.buttonCreateTenant().click(); + page.createTenantForm().inputTenantCode().sendKeys(tenant); + page.createTenantForm().inputDescription().sendKeys("Test"); + page.createTenantForm().buttonSubmit().click(); + + await().untilAsserted(() -> assertThat(browser.findElementByTagName("body") + .getText().contains("already exists")) + .as("Should fail when creating a duplicate tenant") + .isTrue()); + + page.createTenantForm().buttonCancel().click(); + } + + @Test + @Order(30) + void testDeleteTenant() { + final String tenant = System.getProperty("user.name"); + final TenantPage page = new TenantPage(browser); + + page.tenantList() + .stream() + .filter(it -> it.getText().contains(tenant)) + .findFirst() + .ifPresent(it -> it.findElement(By.className("delete")).click()); + + page.buttonConfirm().click(); + } +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java new file mode 100644 index 0000000000..a772517204 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/LoginPage.java @@ -0,0 +1,47 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.pages; + +import org.openqa.selenium.WebElement; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.PageFactory; + +import lombok.Getter; + +@Getter +public final class LoginPage { + private final RemoteWebDriver driver; + + @FindBy(id = "input-username") + private WebElement inputUsername; + + @FindBy(id = "input-password") + private WebElement inputPassword; + + @FindBy(id = "button-login") + private WebElement buttonLogin; + + public LoginPage(RemoteWebDriver driver) { + this.driver = driver; + + PageFactory.initElements(driver, this); + } +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java new file mode 100644 index 0000000000..da973490d3 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/java/org/apache/dolphinscheduler/e2e/pages/TenantPage.java @@ -0,0 +1,78 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.pages; + +import java.util.List; + +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; +import org.openqa.selenium.support.FindBys; +import org.openqa.selenium.support.PageFactory; + +import lombok.Getter; + +@Getter +public final class TenantPage { + private final WebDriver driver; + + @FindBy(id = "button-create-tenant") + private WebElement buttonCreateTenant; + + @FindBy(className = "rows-tenant") + private List tenantList; + + @FindBys({ + @FindBy(className = "el-popconfirm"), + @FindBy(className = "el-button--primary"), + }) + private WebElement buttonConfirm; + + private final CreateTenantForm createTenantForm; + + public TenantPage(WebDriver driver) { + this.driver = driver; + this.createTenantForm = new CreateTenantForm(); + + PageFactory.initElements(driver, this); + } + + @Getter + public class CreateTenantForm { + CreateTenantForm() { + PageFactory.initElements(driver, this); + } + + @FindBy(id = "input-tenant-code") + private WebElement inputTenantCode; + + @FindBy(id = "select-queue") + private WebElement selectQueue; + + @FindBy(id = "input-description") + private WebElement inputDescription; + + @FindBy(id = "button-submit") + private WebElement buttonSubmit; + + @FindBy(id = "button-cancel") + private WebElement buttonCancel; + } +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml new file mode 100644 index 0000000000..13075d3c94 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-case/src/test/resources/docker/tenant/docker-compose.yaml @@ -0,0 +1,35 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +version: "2.1" + +services: + dolphinscheduler: + image: apache/dolphinscheduler:ci + command: [ standalone-server ] + expose: + - 12345 + networks: + - e2e + healthcheck: + test: [ "CMD", "bash", "-c", "cat < /dev/null > /dev/tcp/127.0.0.1/12345" ] + interval: 5s + timeout: 60s + retries: 120 + +networks: + e2e: diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml new file mode 100644 index 0000000000..060810c5cf --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/pom.xml @@ -0,0 +1,32 @@ + + + + + + dolphinscheduler-e2e + org.apache.dolphinscheduler + 1.0-SNAPSHOT + + 4.0.0 + + dolphinscheduler-e2e-core + diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java new file mode 100644 index 0000000000..8d49ca3b91 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinScheduler.java @@ -0,0 +1,41 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Inherited +@Testcontainers +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@TestMethodOrder(OrderAnnotation.class) +@ExtendWith(DolphinSchedulerExtension.class) +public @interface DolphinScheduler { + String[] composeFiles(); +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java new file mode 100644 index 0000000000..d7f3232675 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/DolphinSchedulerExtension.java @@ -0,0 +1,187 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.core; + +import static org.testcontainers.containers.BrowserWebDriverContainer.VncRecordingMode.RECORD_ALL; +import static org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat.MP4; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.testcontainers.containers.BrowserWebDriverContainer; +import org.testcontainers.containers.ContainerState; +import org.testcontainers.containers.DockerComposeContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.shaded.org.apache.commons.lang.SystemUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; + +import com.google.common.base.Strings; +import com.google.common.net.HostAndPort; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +final class DolphinSchedulerExtension + implements BeforeAllCallback, AfterAllCallback, + BeforeEachCallback { + private final boolean LOCAL_MODE = Objects.equals(System.getProperty("local"), "true"); + + private RemoteWebDriver driver; + private DockerComposeContainer compose; + private BrowserWebDriverContainer browser; + + @Override + @SuppressWarnings("UnstableApiUsage") + public void beforeAll(ExtensionContext context) throws IOException { + Awaitility.setDefaultTimeout(Duration.ofSeconds(5)); + Awaitility.setDefaultPollInterval(Duration.ofSeconds(1)); + + Network network = null; + HostAndPort address = null; + String rootPath = "/"; + if (!LOCAL_MODE) { + compose = createDockerCompose(context); + compose.start(); + + final ContainerState dsContainer = compose.getContainerByServiceName("dolphinscheduler_1") + .orElseThrow(() -> new RuntimeException("Failed to find a container named 'dolphinscheduler'")); + final String networkId = dsContainer.getContainerInfo().getNetworkSettings().getNetworks().keySet().iterator().next(); + network = new Network() { + @Override + public String getId() { + return networkId; + } + + @Override + public void close() { + } + + @Override + public Statement apply(Statement base, Description description) { + return null; + } + }; + address = HostAndPort.fromParts("dolphinscheduler", 12345); + rootPath = "/dolphinscheduler"; + } + + final Path record; + if (!Strings.isNullOrEmpty(System.getenv("RECORDING_PATH"))) { + record = Paths.get(System.getenv("RECORDING_PATH")); + if (!record.toFile().exists()) { + if (!record.toFile().mkdir()) { + throw new IOException("Failed to create recording directory: " + record.toAbsolutePath()); + } + } + } else { + record = Files.createTempDirectory("record-"); + } + browser = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withRecordingMode(RECORD_ALL, record.toFile(), MP4); + if (network != null) { + browser.withNetwork(network); + } + browser.start(); + + driver = browser.getWebDriver(); + + driver.manage().timeouts() + .implicitlyWait(5, TimeUnit.SECONDS) + .pageLoadTimeout(5, TimeUnit.SECONDS); + if (address == null) { + try { + address = HostAndPort.fromParts(browser.getTestHostIpAddress(), 8888); + } catch (UnsupportedOperationException ignored) { + if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_MAC_OSX) { + address = HostAndPort.fromParts("host.docker.internal", 8888); + } + } + } + if (address == null) { + throw new UnsupportedOperationException("Unsupported operation system"); + } + driver.get(new URL("http", address.getHost(), address.getPort(), rootPath).toString()); + + browser.beforeTest(new TestDescription(context)); + } + + @Override + public void afterAll(ExtensionContext context) { + browser.afterTest(new TestDescription(context), Optional.empty()); + browser.stop(); + if (compose != null) { + compose.stop(); + } + } + + @Override + public void beforeEach(ExtensionContext context) { + final Object instance = context.getRequiredTestInstance(); + Stream.of(instance.getClass().getDeclaredFields()) + .filter(f -> WebDriver.class.isAssignableFrom(f.getType())) + .forEach(it -> { + try { + it.setAccessible(true); + it.set(instance, driver); + } catch (IllegalAccessException e) { + LOGGER.error("Failed to inject web driver to field: {}", it.getName(), e); + } + }); + } + + private DockerComposeContainer createDockerCompose(ExtensionContext context) { + final Class clazz = context.getRequiredTestClass(); + final DolphinScheduler annotation = clazz.getAnnotation(DolphinScheduler.class); + final List files = Stream.of(annotation.composeFiles()) + .map(it -> DolphinScheduler.class.getClassLoader().getResource(it)) + .filter(Objects::nonNull) + .map(URL::getPath) + .map(File::new) + .collect(Collectors.toList()); + compose = new DockerComposeContainer<>(files) + .withPull(true) + .withTailChildContainers(true) + .waitingFor("dolphinscheduler_1", Wait.forHealthcheck()); + + return compose; + } +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java new file mode 100644 index 0000000000..16f366cd83 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/java/org/apache/dolphinscheduler/e2e/core/TestDescription.java @@ -0,0 +1,55 @@ +/* + * Licensed to Apache Software Foundation (ASF) under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Apache Software Foundation (ASF) licenses this file to you 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 org.apache.dolphinscheduler.e2e.core; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import static org.junit.platform.commons.util.StringUtils.isBlank; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import org.junit.jupiter.api.extension.ExtensionContext; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +final class TestDescription implements org.testcontainers.lifecycle.TestDescription { + private static final String UNKNOWN_NAME = "unknown"; + + private final ExtensionContext context; + + @Override + public String getTestId() { + return context.getUniqueId(); + } + + @Override + public String getFilesystemFriendlyName() { + final String contextId = context.getUniqueId(); + try { + return (isBlank(contextId)) + ? UNKNOWN_NAME + : URLEncoder.encode(contextId, UTF_8.toString()); + } catch (UnsupportedEncodingException e) { + return UNKNOWN_NAME; + } + } +} diff --git a/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..167e6e6d97 --- /dev/null +++ b/dolphinscheduler-e2e/dolphinscheduler-e2e-core/src/main/resources/log4j2.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + diff --git a/dolphinscheduler-e2e/lombok.config b/dolphinscheduler-e2e/lombok.config new file mode 100644 index 0000000000..0056b8f78b --- /dev/null +++ b/dolphinscheduler-e2e/lombok.config @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# + +lombok.accessors.fluent=true +lombok.log.fieldname=LOGGER +lombok.accessors.fluent=true diff --git a/dolphinscheduler-e2e/pom.xml b/dolphinscheduler-e2e/pom.xml new file mode 100644 index 0000000000..cb841af896 --- /dev/null +++ b/dolphinscheduler-e2e/pom.xml @@ -0,0 +1,138 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-e2e + pom + 1.0-SNAPSHOT + + + dolphinscheduler-e2e-core + dolphinscheduler-e2e-case + + + + 8 + 8 + UTF-8 + + 5.7.2 + 3.141.59 + 1.18.20 + 3.20.2 + 4.1.0 + 1.5.30 + + + + + org.slf4j + slf4j-api + 1.7.30 + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.14.1 + + + + org.junit.jupiter + junit-jupiter + + + + org.testcontainers + testcontainers + + + org.testcontainers + junit-jupiter + + + + org.testcontainers + selenium + + + + org.seleniumhq.selenium + selenium-chrome-driver + ${selenium.version} + + + org.seleniumhq.selenium + selenium-support + ${selenium.version} + + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + org.awaitility + awaitility + ${awaitility.version} + test + + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + + org.junit + junit-bom + ${junit.version} + import + pom + + + org.testcontainers + testcontainers-bom + 1.16.0 + import + pom + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + + diff --git a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java index bc4287119d..e5a9b00294 100644 --- a/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java +++ b/dolphinscheduler-standalone-server/src/main/java/org/apache/dolphinscheduler/server/StandaloneServer.java @@ -125,6 +125,8 @@ public class StandaloneServer { if (Files.exists(taskPluginPath)) { System.setProperty("task.plugin.binding", taskPluginPath.toString()); System.setProperty("task.plugin.dir", ""); + } else { + System.setProperty("task.plugin.binding", "lib/plugin/task/shell"); } } } diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue index 3bc83a0284..1e9b125b89 100644 --- a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue +++ b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/_source/createTenement.vue @@ -16,6 +16,8 @@ */ diff --git a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue index 5c82073565..d6c522690a 100644 --- a/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue +++ b/dolphinscheduler-ui/src/js/conf/home/pages/security/pages/tenement/index.vue @@ -19,7 +19,7 @@