diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..9540ee2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,85 @@ +name: Build and test + +on: + push: + + pull_request: + branches: [ develop ] + + workflow_dispatch: + + repository_dispatch: + types: [utPLSQL-build,utPLSQL-java-api-build] + +defaults: + run: + shell: bash + +jobs: + build: + name: Test on JDK ${{ matrix.jdk }} with utPLSQL ${{ matrix.utplsql_version }} + runs-on: ubuntu-latest + env: + ORACLE_VERSION: "gvenzl/oracle-xe:18.4.0-slim" + UTPLSQL_VERSION: ${{matrix.utplsql_version}} + UTPLSQL_FILE: ${{matrix.utplsql_file}} + ORACLE_PASSWORD: oracle + DB_URL: "127.0.0.1:1521:XE" + DB_USER: app + DB_PASS: app + + strategy: + fail-fast: false + matrix: + utplsql_version: ["v3.0.1","v3.0.2","v3.0.3","v3.0.4","v3.1.1","v3.1.2","v3.1.3","v3.1.6","v3.1.7","v3.1.8","v3.1.9","v3.1.10","v3.1.11","develop"] + utplsql_file: ["utPLSQL"] + jdk: ['8'] + include: + - utplsql_version: "v3.0.0" + jdk: '8' + utplsql_file: "utPLSQLv3.0.0" + services: + oracle: + image: gvenzl/oracle-xe:18.4.0-slim + env: + ORACLE_PASSWORD: oracle + ports: + - 1521:1521 + options: >- + --health-cmd healthcheck.sh + --health-interval 10s + --health-timeout 5s + --health-retries 10 + --name oracle + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: ${{matrix.jdk}} + + - name: Install utplsql + run: .travis/install_utplsql.sh + + - name: Install demo project + run: .travis/install_demo_project.sh + + - name: Build and Test + run: mvn verify jar:jar appassembler:assemble + + slack-workflow-status: + if: always() + name: Post Workflow Status To Slack + needs: [ build ] + runs-on: ubuntu-latest + steps: + - name: Slack Workflow Notification + uses: Gamesight/slack-workflow-status@master + with: + repo_token: ${{secrets.GITHUB_TOKEN}} + slack_webhook_url: ${{secrets.SLACK_WEBHOOK_URL}} + name: 'Github Actions[bot]' + icon_url: 'https://octodex.github.com/images/mona-the-rivetertocat.png' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bf6c098 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + tags: + - "*.*.*" + + workflow_dispatch: + +jobs: + + release: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - uses: actions/setup-java@v2 + with: + distribution: 'adopt' + java-version: 8 + + - name: Build Release + run: mvn package appassembler:assemble assembly:single checksum:files -DskipTests + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + target/utPLSQL-cli.zip + target/utPLSQL-cli.zip.md5 + + slack-workflow-status: + if: always() + name: Post Workflow Status To Slack + needs: [ release ] + runs-on: ubuntu-latest + steps: + - name: Slack Workflow Notification + uses: Gamesight/slack-workflow-status@master + with: + repo_token: ${{secrets.GITHUB_TOKEN}} + slack_webhook_url: ${{secrets.SLACK_WEBHOOK_URL}} + name: 'Github Actions[bot]' + icon_url: 'https://octodex.github.com/images/mona-the-rivetertocat.png' diff --git a/.gitignore b/.gitignore index 2d0c11c..b4762f1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,8 @@ buildNumber.properties # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) !/.mvn/wrapper/maven-wrapper.jar + +# Project exclude paths +/.gradle/ +/build/ +/out/ \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..4f717ff --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/dictionaries/utPLSQL.xml b/.idea/dictionaries/utPLSQL.xml new file mode 100644 index 0000000..6cb7c83 --- /dev/null +++ b/.idea/dictionaries/utPLSQL.xml @@ -0,0 +1,7 @@ + + + + utplsql + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5ef4630..76b64a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ services: - docker jdk: - - oraclejdk8 + - openjdk8 env: global: @@ -19,6 +19,22 @@ env: - DB_PASS=app - ORACLE_VERSION="11g-r2-xe" - DOCKER_OPTIONS="--shm-size=1g" + - UTPLSQL_FILE="utPLSQL" + matrix: + - UTPLSQL_VERSION="v3.0.0" + UTPLSQL_FILE="utPLSQLv3.0.0" + - UTPLSQL_VERSION="v3.0.1" + - UTPLSQL_VERSION="v3.0.2" + - UTPLSQL_VERSION="v3.0.3" + - UTPLSQL_VERSION="v3.0.4" + - UTPLSQL_VERSION="v3.1.1" + - UTPLSQL_VERSION="v3.1.2" + - UTPLSQL_VERSION="v3.1.3" + - UTPLSQL_VERSION="v3.1.6" + - UTPLSQL_VERSION="v3.1.7" + - UTPLSQL_VERSION="v3.1.8" + - UTPLSQL_VERSION="develop" + UTPLSQL_FILE="utPLSQL" cache: directories: @@ -27,14 +43,13 @@ cache: - $MAVEN_CFG install: - - bash .travis/maven_cfg.sh - bash .travis/start_db.sh - bash .travis/install_utplsql.sh - bash .travis/install_demo_project.sh script: - mvn package -DskipTests - - mvn package jar:jar appassembler:assemble + - mvn package verify jar:jar appassembler:assemble before_deploy: - bash .travis/create_release.sh @@ -43,19 +58,26 @@ before_deploy: deploy: - provider: releases api_key: $GITHUB_API_TOKEN - file: utPLSQL-cli.zip + file: + - utPLSQL-cli.zip + - utPLSQL-cli.zip.md5 skip_cleanup: true on: repository: utPLSQL/utPLSQL-cli tags: true + # Use only first job "#xxx.1" to publish artifacts + condition: "${TRAVIS_JOB_NUMBER} =~ \\.1$" - provider: bintray file: bintray.json user: ${BINTRAY_USER} key: ${BINTRAY_API_KEY} dry-run: false + skip_cleanup: true on: repository: utPLSQL/utPLSQL-cli branch: develop + # Use only first job "#xxx.1" to publish artifacts + condition: "${TRAVIS_JOB_NUMBER} =~ \\.1$" notifications: slack: diff --git a/.travis/create_api_user.sh b/.travis/create_api_user.sh old mode 100644 new mode 100755 diff --git a/.travis/create_release.sh b/.travis/create_release.sh old mode 100644 new mode 100755 index d1f36ca..cc6d14d --- a/.travis/create_release.sh +++ b/.travis/create_release.sh @@ -2,31 +2,31 @@ set -ev VERSION=`date +%Y%m%d%H%M` +MVN_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +# Strip -SNAPSHOT if exists +MVN_VERSION=${MVN_VERSION/-SNAPSHOT/} #overcome maven assemble issue: https://github.com/mojohaus/appassembler/issues/61 sed -i '/CYGWIN\*) cygwin=true/c\ CYGWIN*|MINGW*) cygwin=true ;;' target/appassembler/bin/utplsql mkdir dist mv target/appassembler utPLSQL-cli -# Remove Oracle libraries du to licensing problems -rm -f utPLSQL-cli/lib/ucp*.jar -rm -f utPLSQL-cli/lib/ojdbc8*.jar -rm -f utPLSQL-cli/lib/orai18n*.jar zip -r -q dist/utPLSQL-cli-${TRAVIS_BRANCH}-${VERSION}.zip utPLSQL-cli zip -r -q utPLSQL-cli.zip utPLSQL-cli +md5sum utPLSQL-cli.zip --tag > utPLSQL-cli.zip.md5 cat > bintray.json < install.sh.tmp < - - - - - - - - maven.oracle.com - ${env.ORACLE_OTN_USER} - ${env.ORACLE_OTN_PASSWORD} - - - ANY - ANY - OAM 11g - - - - - - http.protocol.allow-circular-redirects - %b,true - - - - - - - - - diff --git a/.travis/start_db.sh b/.travis/start_db.sh old mode 100644 new mode 100755 diff --git a/README.md b/README.md index fd88eed..fcb6dee 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,201 @@ +[![latest-release](https://img.shields.io/github/release/utPLSQL/utPLSQL-cli.svg)](https://github.com/utPLSQL/utPLSQL-cli/releases) +[![license](https://img.shields.io/github/license/utPLSQL/utPLSQL-cli.svg)](https://www.apache.org/licenses/LICENSE-2.0) +[![Build status](https://github.com/utPLSQL/utPLSQL-cli/actions/workflows/build.yml/badge.svg)](https://github.com/utPLSQL/utPLSQL-cli/actions/workflows/build.yml) + +---------- # utPLSQL-cli Java command-line client for [utPLSQL v3](https://github.com/utPLSQL/utPLSQL/). Provides an easy way of invoking utPLSQL from command-line. Main features: * Ability to run tests with multiple reporters simultaneously. +* Realtime reporting during test-run * Ability to save output from every individual reporter to a separate output file. * Allows execution of selected suites, subset of suite. -* Maps project and test files to database objects for reporting purposes. (Comming Soon) +* Maps project and test files to database objects for reporting purposes. ## Downloading Published releases are available for download on the [utPLSQL-cli GitHub Releases Page.](https://github.com/utPLSQL/utPLSQL-cli/releases) -You can also download all development versions from [Bintray](https://bintray.com/viniciusam/utPLSQL-cli/utPLSQL-cli-develop#files). +You can also download all development versions from [Bintray](https://bintray.com/utplsql/utPLSQL-cli/utPLSQL-cli-develop#files). ## Requirements -* [Java SE Runtime Environment 8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html) +* [Java SE Runtime Environment 8](http://www.oracle.com/technetwork/java/javase/downloads/jre8-downloads-2133155.html) or newer * When using reporters for Sonar or Coveralls client needs to be invoked from project's root directory. -* Due to Oracle license we can't ship the necessary oracle libraries directly with utPLSQL-cli. Please download the libraries directly from oracle website and put the jars into the "lib" folder of your utPLSQL-cli installation - * Oracle `ojdbc8` driver - * If you are on a 11g database with non standard NLS settings, you will also need the `orai18n` library. - * All of the above can be downloaded from [Oracle download site](http://www.oracle.com/technetwork/database/features/jdbc/jdbc-ucp-122-3110062.html) ## Compatibility -The latest CLI is always compatible with all database frameworks of the same minor version. -For example CLI-3.0.4 is compatible with database framework 3.0.0-3.0.4 but not with database framework 2.x and 3.1.x. +The latest CLI is always compatible with all database frameworks of the same major version. +For example CLI-3.1.0 is compatible with database framework 3.0.0-3.1.* but not with database framework 2.x. + +## Localization and NLS settings +utPLSQL-cli will use the environment variables "LC_ALL" or "LANG" to change the locale and therefore the client NLS settings. +If neither environment variable is available, it will use the JVM default locale. + +Example: to change the NLS-settings to English American, you can do the following: +``` +export LC_ALL=en_US.utf-8 +``` + +The charset-part of LC_ALL is ignored. + +In addition, utPLSQL-cli will use an existing "NLS_LANG" environment variable to create corresponding +`ALTER SESSION`-statements during initialization of the connection. + +The variable is parsed according to the [Oracle globalization documentation](https://www.oracle.com/technetwork/database/database-technologies/globalization/nls-lang-099431.html#_Toc110410543) + +Example: "NLS_LANG" of `AMERICAN_AMERICA.UTF8` will lead to the following statements: +```sql +ALTER SESSION SET NLS_LANGUAGE='AMERICAN'; +ALTER SESSION SET NLS_TERRITORY='AMERICA'; +``` + +## Charset + +Java will use the default charset of your system for any string output. +You can change this by passing the `-Dfile.encoding` property to the JVM when running a java-application. +To avoid changing the utPLSQL-cli shell- or batchscript, you can define `-Dfile.encoding` in the environment variable `JAVA_TOOL_OPTIONS`. +This environment variable will be picked up and interpreted by the JVM: + +``` +export JAVA_TOOL_OPTIONS='-Dfile.encoding=utf8' +utplsql run user/pw@connecstring + +> Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=utf8 +``` + +Make sure that the defined charset matches with the codepage your console is using. ## Usage +Currently, utPLSQL-cli supports the following sub-commands: +- run +- info +- reporters +- help -`utplsql run [-p=(ut_path|ut_paths)] [-f=format [-o=output_file] [-s] ...]` +To get more info about a command, use +``` +utplsql -h +``` +Example: +``` +utplsql run -h +``` + +#### \ + +This is used in all commands as first parameter (though it's optional for `info`). + +Accepted formats: + +- `/@//[:]/` +- `/@::` +- `/@` + +To connect using TNS, you need to have the ORACLE_HOME environment variable set. +The file tnsnames.ora must exist in path %ORACLE_HOME%/network/admin +The file tnsnames.ora must contain valid TNS entries. +In case you use a username containing `/` or a password containing `@` you should encapsulate it with double quotes `"`: ``` - - accepted formats: - /@//[:]/ - /@:: - /@ - To connect using TNS, you need to have the ORACLE_HOME environment variable set. - The file tnsnames.ora must exist in path %ORACLE_HOME%/network/admin - The file tnsnames.ora must contain valid TNS entries. +utplsql run "my/Username"/"myP@ssword"@connectstring +``` + +### run +`utplsql run []` + + +#### Options +``` -p=suite_path(s) - A suite path or a comma separated list of suite paths for unit test to be executed. - The path(s) can be in one of the following formats: +(--path) The path(s) can be in one of the following formats: schema[.package[.procedure]] schema:suite[.suite[.suite][...]][.procedure] Both formats can be mixed in the list. If only schema is provided, then all suites owner by that schema are executed. If -p is omitted, the current schema is used. + +--tags=tags - A comma separated list of tags to include or exclude in the run. + The excluded tags must be preceeded by a `-` (minus) sign and the entire expression must be surrounded by escaped doubleqotes in command line. + Format: --tags=tag1[,tag2,...,tagN] + or + Format: --tags=\"-tag1\"[,\"-tag2\",...,tagN] -f=format - A reporter to be used for reporting. - If no -f option is provided, the default ut_documentation_reporter is used. - Available options: - -f=ut_documentation_reporter - A textual pretty-print of unit test results (usually use for console output) - -f=ut_teamcity_reporter - For reporting live progress of test execution with Teamcity CI. - -f=ut_xunit_reporter - Used for reporting test results with CI servers like Jenkins/Hudson/Teamcity. - -f=ut_coverage_html_reporter - Generates a HTML coverage report with summary and line by line information on code coverage. - Based on open-source simplecov-html coverage reporter for Ruby. - Includes source code in the report. - -f=ut_coveralls_reporter - Generates a JSON coverage report providing information on code coverage with line numbers. - Designed for [Coveralls](https://coveralls.io/). - -f=ut_coverage_sonar_reporter - Generates a JSON coverage report providing information on code coverage with line numbers. - Designed for [SonarQube](https://about.sonarqube.com/) to report coverage. - -f=ut_sonar_test_reporter - Generates a JSON report providing detailed information on test execution. - Designed for [SonarQube](https://about.sonarqube.com/) to report test execution. - - -o=output - Defines file name to save the output from the specified reporter. +(--format) If no -f option is provided, the default ut_documentation_reporter is used. + See reporters command for possible values + -o=output - Defines file name to save the output from the specified reporter. If defined, the output is not displayed on screen by default. This can be changed with the -s parameter. If not defined, then output will be displayed on screen, even if the parameter -s is not specified. If more than one -o parameter is specified for one -f parameter, the last one is taken into consideration. - -s - Forces putting output to to screen for a given -f parameter. + -s - Forces putting output to to screen for a given -f parameter. + -source_path=source - path to project source files, use the following options to enable custom type mappings: - -owner="app" - -regex_expression="pattern" - -type_mapping="matched_string=TYPE[/matched_string=TYPE]*" - -owner_subexpression=subexpression_number - -type_subexpression=subexpression_number - -name_subexpression=subexpression_number + -owner="app" + -regex_expression="pattern" + -type_mapping="matched_string=TYPE[/matched_string=TYPE]*" + -owner_subexpression=subexpression_number + -type_subexpression=subexpression_number + -name_subexpression=subexpression_number + -test_path=test - path to project test files, use the following options to enable custom type mappings: - -owner="app" - -regex_expression="pattern" - -type_mapping="matched_string=TYPE[/matched_string=TYPE]*" - -owner_subexpression=subexpression_number - -type_subexpression=subexpression_number - -name_subexpression=subexpression_number + -owner="app" + -regex_expression="pattern" + -type_mapping="matched_string=TYPE[/matched_string=TYPE]*" + -owner_subexpression=subexpression_number + -type_subexpression=subexpression_number + -name_subexpression=subexpression_number + -c - If specified, enables printing of test results in colors as defined by ANSICONSOLE standards. - Works only on reporeters that support colors (ut_documentation_reporter). ---failure-exit-code - Override the exit code on failure, defaults to 1. You can set it to 0 to always exit with a success status. +(--color) Works only on reporeters that support colors (ut_documentation_reporter). + +-fcode=code - Override the exit code on failure, defaults to 1. You can set it to 0 to always exit with a success status. +(--failure-exit-code) + -scc - If specified, skips the compatibility-check with the version of the database framework. - If you skip compatibility-check, CLI will expect the most actual framework version +(--skip- If you skip compatibility-check, CLI will expect the most actual framework version + compatibility-check) + +-include=pckg_list - Comma-separated object list to include in the coverage report. + Format: [schema.]package[,[schema.]package ...]. + See coverage reporting options in framework documentation. + +-exclude=pckg_list - Comma-separated object list to exclude from the coverage report. + Format: [schema.]package[,[schema.]package ...]. + See coverage reporting options in framework documentation. + +-q - Does not output the informational messages normally printed to console. +(--quiet) Default: false + +-d - Outputs a load of debug information to console +(--debug) Default: false + +-t=timeInMinutes - Sets the timeout in minutes after which the cli will abort. +(--timeout) Default 60 + +-D - Enables DBMS_OUTPUT in the TestRunner-Session +(--dbms_output) Default: false + +-r - Enables random order of test executions +(--random-test-order) Default: false + +-seed - Sets the seed to use for random test execution order. If set, it sets -random to true +(--random-test-order-seed) + +--coverage-schemes - A comma separated list of schemas on which coverage should be gathered + Format: --coverage-schemes=schema1[,schema2[,schema3]] + +--ora-stuck-timeout - Sets a timeout around Reporter creation and retries when not ready after a while. 0 = no timeout. ``` Parameters -f, -o, -s are correlated. That is parameters -o and -s are controlling outputs for reporter specified by the preceding -f parameter. Sonar and Coveralls reporter will only provide valid reports, when source_path and/or test_path are provided, and ut_run is executed from your project's root path. -Examples: +#### Examples ``` -utplsql run hr/hr@xe -p=hr_test -f=ut_documentation_reporter -o=run.log -s -f=ut_coverage_html_reporter -o=coverage.html -source_path=source +> utplsql run hr/hr@xe -p=hr_test -f=ut_documentation_reporter -o=run.log -s -f=ut_coverage_html_reporter -o=coverage.html -source_path=source ``` Invokes all Unit tests from schema/package "hr_test" with two reporters: @@ -111,11 +204,110 @@ Invokes all Unit tests from schema/package "hr_test" with two reporters: * ut_coverage_html_reporter - will report only on database objects that are mapping to file structure from "source" folder and save output to file "coverage.html" ``` -utplsql run hr/hr@xe +> utplsql run hr/hr@xe ``` Invokes all unit test suites from schema "hr". Results are displayed to screen using default ut_documentation_reporter. -##### Enabling Color Outputs on Windows +### info +`utplsql info []` + + +#### Examples + +``` +> utplsql info + +cli 3.1.1-SNAPSHOT.local +utPLSQL-java-api 3.1.1-SNAPSHOT.123 +``` +``` +> utplsql info app/app@localhost:1521/ORCLPDB1 + +cli 3.1.1-SNAPSHOT.local +utPLSQL-java-api 3.1.1-SNAPSHOT.123 +utPLSQL 3.1.2.1913 +``` + +### reporters +`utplsql reporters ` + +#### Examples +``` +> utplsql reporters app/app@localhost:1521/ORCLPDB1 + +UT_COVERAGE_COBERTURA_REPORTER: + Generates a Cobertura coverage report providing information on code coverage with line numbers. + Designed for Jenkins and TFS to report coverage. + Cobertura Document Type Definition can be found: http://cobertura.sourceforge.net/xml/coverage-04.dtd. + Sample file: https://github.com/leobalter/testing-examples/blob/master/solutions/3/report/cobertura-coverage.xml. + +UT_COVERAGE_HTML_REPORTER: + Generates a HTML coverage report with summary and line by line information on code coverage. + Based on open-source simplecov-html coverage reporter for Ruby. + Includes source code in the report. + Will copy all necessary assets to a folder named after the Output-File + +UT_COVERAGE_SONAR_REPORTER: + Generates a JSON coverage report providing information on code coverage with line numbers. + Designed for [SonarQube](https://about.sonarqube.com/) to report coverage. + JSON format returned conforms with the Sonar specification: https://docs.sonarqube.org/display/SONAR/Generic+Test+Data + +UT_COVERALLS_REPORTER: + Generates a JSON coverage report providing information on code coverage with line numbers. + Designed for [Coveralls](https://coveralls.io/). + JSON format conforms with specification: https://docs.coveralls.io/api-introduction + +UT_DEBUG_REPORTER: + No description available + +UT_DOCUMENTATION_REPORTER: + A textual pretty-print of unit test results (usually use for console output) + Provides additional properties lvl and failed + +UT_JUNIT_REPORTER: + Provides outcomes in a format conforming with JUnit 4 and above as defined in: https://gist.github.com/kuzuha/232902acab1344d6b578 + +UT_REALTIME_REPORTER: + Provides test results in a XML format, for clients such as SQL Developer interested in showing progressing details. + +UT_SONAR_TEST_REPORTER: + Generates a JSON report providing detailed information on test execution. + Designed for [SonarQube](https://about.sonarqube.com/) to report test execution. + JSON format returned conforms with the Sonar specification: https://docs.sonarqube.org/display/SONAR/Generic+Test+Data + +UT_TEAMCITY_REPORTER: + Provides the TeamCity (a CI server by jetbrains) reporting-format that allows tracking of progress of a CI step/task as it executes. + https://confluence.jetbrains.com/display/TCD9/Build+Script+Interaction+with+TeamCity + +UT_TFS_JUNIT_REPORTER: + Provides outcomes in a format conforming with JUnit version for TFS / VSTS. + As defined by specs :https://docs.microsoft.com/en-us/vsts/build-release/tasks/test/publish-test-results?view=vsts + Version is based on windy road junit https://github.com/windyroad/JUnit-Schema/blob/master/JUnit.xsd. + +UT_XUNIT_REPORTER: + Depracated reporter. Please use Junit. + Provides outcomes in a format conforming with JUnit 4 and above as defined in: https://gist.github.com/kuzuha/232902acab1344d6b578 +``` + +## Using utPLSQL-cli as sysdba + +Since 3.1.6 it is possible to run utPLSQL-cli as sysdba by running + +``` +utplsql run "sys as sysdba"/pw@connectstring +``` + +It is, however, __not recommended__ to run utPLSQL with sysdba privileges. + +## Enabling Color Outputs on Windows To enable color outputs on Windows cmd you need to install an open-source utility called [ANSICON](http://adoxa.altervista.org/ansicon/). + +## Custom Reporters + +Since v3.1.0 you can call custom reporters (PL/SQL) via cli, too. Just call the name of the custom reporter as you would do for the core reporters. + +``` +utplsql run hr/hr@xe -p=hr_test -f=my_custom_reporter -o=run.log -s +``` diff --git a/pom.xml b/pom.xml index 2f5dd67..b52b2d2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,96 +1,240 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - org.utplsql - cli - 3.0.4-SNAPSHOT - jar + org.utplsql + cli + 3.1.10-SNAPSHOT - cli - http://maven.apache.org + utPLSQL CLI + CLI for running Unit Tests with utPLSQL v3+. + https://github.com/utPLSQL/utPLSQL-cli - - UTF-8 - 1.8 - 1.8 - + + UTF-8 + 1.8 + 1.8 + 5.5.2 + 19.3.0.0 + local + - - - org.utplsql - java-api - 3.0.4 - compile - - - com.oracle.jdbc - ucp - - - - - com.beust - jcommander - 1.69 - compile - - - com.zaxxer - HikariCP - 2.7.2 - compile - - - org.slf4j - slf4j-nop - 1.7.25 - compile - - - junit - junit - 4.12 - test - - + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + - - - - org.codehaus.mojo - appassembler-maven-plugin - 1.10 - - true - true - etc - lib - flat - - - org.utplsql.cli.Cli - utplsql - - - - - - + + + org.utplsql + java-api + 3.1.9 + compile + + + com.oracle.ojdbc + ucp + + + com.oracle.ojdbc + ojdbc8 + + + com.oracle.ojdbc + orai18n + + + + + javax.xml.bind + jaxb-api + 2.3.1 + + + ch.qos.logback + logback-classic + 1.2.11 + + + info.picocli + picocli + 4.6.3 + + + com.oracle.database.jdbc + ojdbc8 + ${oracle.jdbc.version} + compile + + + com.oracle.database.jdbc + ucp + + + + + com.oracle.database.nls + orai18n + ${oracle.jdbc.version} + compile + - - - utplsql-java-api - - https://packagecloud.io/utplsql/utplsql-java-api/maven2 - - - true - - - true - - - + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.hamcrest + hamcrest + 2.1 + test + + + + + + org.codehaus.mojo + appassembler-maven-plugin + 1.10 + + true + true + etc + lib + flat + + + org.utplsql.cli.Cli + utplsql + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + + **/*IT.java + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.0 + + + + integration-test + verify + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + utPLSQL-cli + false + + src/main/assembly/zip.xml + + + + + net.nicoulaj.maven.plugins + checksum-maven-plugin + 1.10 + + + + ${project.build.directory} + + utPLSQL-cli.zip + + + + + MD5 + + + + + com.amashchenko.maven.plugin + gitflow-maven-plugin + 1.18.0 + + true + + main + + + + + + + src/main/resources + true + + **/utplsql-cli.version + + + + src/main/resources + false + + **/utplsql-cli.version + + + + + + + + utplsql-java-api + + https://packagecloud.io/utplsql/utplsql-java-api/maven2 + + + true + + + true + + + + + + + utPLSQL-local + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + ${dbUrl} + ${dbUser} + ${dbPass} + + + + + + + + diff --git a/src/main/assembly/zip.xml b/src/main/assembly/zip.xml new file mode 100644 index 0000000..6d16b57 --- /dev/null +++ b/src/main/assembly/zip.xml @@ -0,0 +1,19 @@ + + + zip + + false + + + zip + + + + + ${project.build.directory}/appassembler + /utPLSQL-cli + + + diff --git a/src/main/java/org/utplsql/cli/Cli.java b/src/main/java/org/utplsql/cli/Cli.java index bdfbac3..4f711b6 100644 --- a/src/main/java/org/utplsql/cli/Cli.java +++ b/src/main/java/org/utplsql/cli/Cli.java @@ -1,56 +1,68 @@ package org.utplsql.cli; -import com.beust.jcommander.JCommander; -import com.beust.jcommander.Parameter; -import com.beust.jcommander.ParameterException; -import org.utplsql.api.exception.DatabaseNotCompatibleException; -import org.utplsql.api.exception.UtPLSQLNotInstalledException; -import org.utplsql.cli.exception.DatabaseConnectionFailed; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import picocli.CommandLine; + +import java.util.List; public class Cli { - public static final int DEFAULT_ERROR_CODE = 1; + private static final Logger logger = LoggerFactory.getLogger(Cli.class); - public static final String HELP_CMD = "-h"; - public static final String RUN_CMD = "run"; + static final int DEFAULT_ERROR_CODE = 1; public static void main(String[] args) { - JCommander jc = new JCommander(); - // jc.addCommand(HELP_CMD, new HelpCommand()); - RunCommand runCmd = new RunCommand(); - jc.addCommand(RUN_CMD, runCmd); + + int exitCode = runPicocliWithExitCode(args); + + System.exit(exitCode); + } + + static int runPicocliWithExitCode(String[] args) { + + logger.debug("Args: "+String.join(", ", args)); + + CommandLine commandLine = new CommandLine(UtplsqlPicocliCommand.class); + commandLine.setTrimQuotes(true); int exitCode = DEFAULT_ERROR_CODE; try { - jc.parse(args); - if (RUN_CMD.equals(jc.getParsedCommand())) { - exitCode = runCmd.run(); - } else { - throw new ParameterException("Command not specified."); + List parsedLines = commandLine.parse(args); + + boolean commandWasRun = false; + for (CommandLine parsedLine : parsedLines) { + if (parsedLine.isUsageHelpRequested()) { + parsedLine.usage(System.out); + return 0; + } else if (parsedLine.isVersionHelpRequested()) { + parsedLine.printVersionHelp(System.out); + return 0; + } + + Object command = parsedLine.getCommand(); + if (command instanceof ICommand) { + exitCode = ((ICommand) command).run(); + commandWasRun = true; + break; + } + } + + if (!commandWasRun) { + commandLine.usage(System.out); } - } catch (ParameterException e) { - if (jc.getParsedCommand() != null) { - System.err.println(e.getMessage()); - jc.usage(jc.getParsedCommand()); - } else { - jc.usage(); + } catch (CommandLine.ParameterException e) { + System.err.println(e.getMessage()); + if (!CommandLine.UnmatchedArgumentException.printSuggestions(e, System.err)) { + e.getCommandLine().usage(System.err); } - } catch ( DatabaseNotCompatibleException | UtPLSQLNotInstalledException | DatabaseConnectionFailed e ) { - System.out.println(e.getMessage()); } catch (Exception e) { e.printStackTrace(); } - System.exit(exitCode); - } - - private static class HelpCommand { - - @Parameter(names = {HELP_CMD, "--help"}, help = true) - public boolean callHelp; - + return exitCode; } } diff --git a/src/main/java/org/utplsql/cli/CliVersionInfo.java b/src/main/java/org/utplsql/cli/CliVersionInfo.java new file mode 100644 index 0000000..72cc407 --- /dev/null +++ b/src/main/java/org/utplsql/cli/CliVersionInfo.java @@ -0,0 +1,41 @@ +package org.utplsql.cli; + +import org.utplsql.api.JavaApiVersionInfo; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * This class is getting updated automatically by the build process. + * Please do not update its constants manually cause they will be overwritten. + * + * @author pesse + */ +public class CliVersionInfo { + + private static final String MAVEN_PROJECT_NAME = "utPLSQL-cli"; + private static String MAVEN_PROJECT_VERSION = "unknown"; + + static { + try { + try (InputStream in = JavaApiVersionInfo.class.getClassLoader().getResourceAsStream("utplsql-cli.version"); + BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + MAVEN_PROJECT_VERSION = reader.readLine(); + } + } catch (IOException e) { + System.out.println("WARNING: Could not get Version information!"); + } + } + + public static String getVersion() { + return MAVEN_PROJECT_VERSION; + } + + public static String getInfo() { + return MAVEN_PROJECT_NAME + " " + getVersion(); + } + + +} diff --git a/src/main/java/org/utplsql/cli/ConnectionConfig.java b/src/main/java/org/utplsql/cli/ConnectionConfig.java new file mode 100644 index 0000000..05e2de7 --- /dev/null +++ b/src/main/java/org/utplsql/cli/ConnectionConfig.java @@ -0,0 +1,54 @@ +package org.utplsql.cli; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ConnectionConfig { + + private final String user; + private final String password; + private final String connect; + + public ConnectionConfig(String connectString) { + Matcher m = Pattern.compile("^(\".+\"|[^/]+)/(\".+\"|[^@]+)@(.*)$").matcher(connectString); + if (m.find()) { + user = stripEnclosingQuotes(m.group(1)); + password = stripEnclosingQuotes(m.group(2)); + connect = m.group(3); + } else { + throw new IllegalArgumentException("Not a valid connectString: '" + connectString + "'"); + } + } + + private String stripEnclosingQuotes(String value) { + if (value.length() > 1 + && value.startsWith("\"") + && value.endsWith("\"")) { + return value.substring(1, value.length() - 1); + } else { + return value; + } + } + + public String getConnect() { + return connect; + } + + public String getUser() { + return user; + } + + public String getPassword() { + return password; + } + + public String getConnectString() { + return user + "/" + password + "@" + connect; + } + + public boolean isSysDba() { + return user != null && + (user.toLowerCase().endsWith(" as sysdba") + || user.toLowerCase().endsWith(" as sysoper")); + } +} diff --git a/src/main/java/org/utplsql/cli/ConnectionInfo.java b/src/main/java/org/utplsql/cli/ConnectionInfo.java deleted file mode 100644 index d96a67f..0000000 --- a/src/main/java/org/utplsql/cli/ConnectionInfo.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.utplsql.cli; - -import com.beust.jcommander.IStringConverter; -import com.zaxxer.hikari.HikariDataSource; - -import java.io.File; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; - -public class ConnectionInfo { - - private String databaseVersion; - - static { - String oracleHome = System.getenv("ORACLE_HOME"); - if (oracleHome != null) { - System.setProperty("oracle.net.tns_admin", - String.join(File.separator, oracleHome, "NETWORK", "ADMIN")); - } - } - - private HikariDataSource pds = new HikariDataSource(); - - public ConnectionInfo(String connectionInfo) { - - pds.setJdbcUrl("jdbc:oracle:thin:" + connectionInfo); - pds.setAutoCommit(false); - - } - - public Connection getConnection() throws SQLException { - return pds.getConnection(); - } - - public static class ConnectionStringConverter implements IStringConverter { - - @Override - public ConnectionInfo convert(String s) { - return new ConnectionInfo(s); - } - } - - public String getOracleDatabaseVersion() throws SQLException - { - try ( Connection conn = getConnection() ) { - return getOracleDatabaseVersion(conn); - } - } - - public String getOracleDatabaseVersion( Connection conn ) throws SQLException - { - if ( databaseVersion == null ) { - databaseVersion = getOracleDatabaseVersionFromConnection( conn ); - } - - return databaseVersion; - } - - /** TODO: Outsource this to Java-API - * - * @param conn - * @return - * @throws SQLException - */ - public static String getOracleDatabaseVersionFromConnection( Connection conn ) throws SQLException { - assert conn != null; - String result = null; - try (PreparedStatement stmt = conn.prepareStatement("select version from product_component_version where product like 'Oracle Database%'")) - { - ResultSet rs = stmt.executeQuery(); - - if ( rs.next() ) - result = rs.getString(1); - } - - return result; - } - -} diff --git a/src/main/java/org/utplsql/cli/DataSourceProvider.java b/src/main/java/org/utplsql/cli/DataSourceProvider.java new file mode 100644 index 0000000..4eee258 --- /dev/null +++ b/src/main/java/org/utplsql/cli/DataSourceProvider.java @@ -0,0 +1,49 @@ +package org.utplsql.cli; + +import org.utplsql.cli.datasource.TestedDataSourceProvider; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.SQLException; + +/** + * Helper class to give you a ready-to-use datasource + * + * @author pesse + */ +public class DataSourceProvider { + + static { + String oracleHome = System.getenv("ORACLE_HOME"); + if (oracleHome != null && System.getProperty("oracle.net.tns_admin") == null) { + System.setProperty("oracle.net.tns_admin", + String.join(File.separator, oracleHome, "NETWORK", "ADMIN")); + } + } + + public static DataSource getDataSource(String connectString, int maxConnections) throws SQLException { + + requireOjdbc(); + + ConnectionConfig config = new ConnectionConfig(connectString); + warnIfSysDba(config); + + return new TestedDataSourceProvider(config, maxConnections).getDataSource(); + } + + private static void requireOjdbc() { + if (!OracleLibraryChecker.checkOjdbcExists()) { + System.out.println("Could not find Oracle JDBC driver in classpath. Please download the jar from Oracle website" + + " and copy it to the 'lib' folder of your utPLSQL-cli installation."); + System.out.println("Download from http://www.oracle.com/technetwork/database/features/jdbc/jdbc-ucp-122-3110062.html"); + + throw new RuntimeException("Can't run utPLSQL-cli without Oracle JDBC driver"); + } + } + + private static void warnIfSysDba(ConnectionConfig config) { + if (config.isSysDba()) { + System.out.println("WARNING: You are connecting to the database as SYSDBA or SYSOPER, which is NOT RECOMMENDED and can put your database at risk!"); + } + } +} diff --git a/src/main/java/org/utplsql/cli/FileWalker.java b/src/main/java/org/utplsql/cli/FileWalker.java index 3fec655..3d07ba6 100644 --- a/src/main/java/org/utplsql/cli/FileWalker.java +++ b/src/main/java/org/utplsql/cli/FileWalker.java @@ -16,8 +16,9 @@ public List getFileList(File baseDir, String inspectPath) { public List getFileList(File baseDir, String inspectPath, boolean relative) { File inspectDir = new File(baseDir, inspectPath); - if (!inspectDir.isDirectory()) + if (!inspectDir.isDirectory()) { throw new IllegalArgumentException(inspectPath + " is not a directory."); + } List fileList = new ArrayList<>(); listDirFiles(baseDir, inspectDir, fileList, relative); @@ -28,15 +29,17 @@ public List getFileList(File baseDir, String inspectPath, boolean relati private void listDirFiles(File baseDir, File directory, List fileList, boolean relative) { File[] directoryFiles = directory.listFiles(); - if (directoryFiles == null) + if (directoryFiles == null) { return; + } for (File file : directoryFiles) { if (file.isFile()) { String absolutePath = file.getAbsolutePath(); - if (relative) + if (relative) { absolutePath = absolutePath.substring(baseDir.getAbsolutePath().length() + 1); + } fileList.add(absolutePath); } else { diff --git a/src/main/java/org/utplsql/cli/ICommand.java b/src/main/java/org/utplsql/cli/ICommand.java new file mode 100644 index 0000000..0ea1813 --- /dev/null +++ b/src/main/java/org/utplsql/cli/ICommand.java @@ -0,0 +1,17 @@ +package org.utplsql.cli; + +/** + * This is the very basic interface that should be implemented by all utPLSQL cli commands + * + * @author pesse + */ +public interface ICommand { + + /** + * We expect the command to handle all eventually occurring exceptions + * and return an exit code + * + * @return exit code integer + */ + int run(); +} diff --git a/src/main/java/org/utplsql/cli/IRunCommand.java b/src/main/java/org/utplsql/cli/IRunCommand.java new file mode 100644 index 0000000..6aa2681 --- /dev/null +++ b/src/main/java/org/utplsql/cli/IRunCommand.java @@ -0,0 +1,15 @@ +package org.utplsql.cli; + +import org.utplsql.api.TestRunner; +import org.utplsql.api.reporter.Reporter; + +import java.util.List; + +public interface IRunCommand extends ICommand { + + void initLogger(); + + TestRunner newTestRunner(List reporterList); + + List getReporterOptionsList(); +} diff --git a/src/main/java/org/utplsql/cli/LocaleInitializer.java b/src/main/java/org/utplsql/cli/LocaleInitializer.java new file mode 100644 index 0000000..bcb0fe7 --- /dev/null +++ b/src/main/java/org/utplsql/cli/LocaleInitializer.java @@ -0,0 +1,74 @@ +package org.utplsql.cli; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.utplsql.api.EnvironmentVariableUtil; + +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class makes sure the java locale is set according to the environment variables LC_ALL and LANG + * We experienced that, in some cases, the locale was not set as expected, therefore this class implements some clear + * rules: + * 1. If environment variable LC_ALL is set, we try to parse its content and set locale according to its value if valid + * 2. If environment variable LANG is set, we try to parse its content and set locale according to its value if valid + * 3. Otherwise we use default locale + * + * @author pesse + */ +class LocaleInitializer { + + private static final Logger logger = LoggerFactory.getLogger(RunAction.class); + + private static final Pattern REGEX_LOCALE = Pattern.compile("^([a-zA-Z]+)[_-]([a-zA-Z]+)"); // We only need the very first part and are pretty forgiving in parsing + + /** + * Sets the default locale according to the rules described above + */ + static void initLocale() { + + boolean localeChanged = setDefaultLocale(EnvironmentVariableUtil.getEnvValue("LC_ALL")); + if (!localeChanged) { + localeChanged = setDefaultLocale(EnvironmentVariableUtil.getEnvValue("LANG")); + } + if ( !localeChanged ) { + logger.debug("Java Locale not changed from LC_ALL or LANG environment variable"); + } + } + + /** + * Set the default locale from a given string like LC_ALL or LANG environment variable + * + * @param localeString Locale-string from LC_ALL or LANG, e.g "en_US.utf-8" + * @return true if successful, false if not + */ + private static boolean setDefaultLocale(String localeString) { + if (localeString == null || localeString.isEmpty()) { + return false; + } + + try { + Matcher m = REGEX_LOCALE.matcher(localeString); + if (m.find()) { + StringBuilder sb = new StringBuilder(); + sb.append(m.group(1)); + if (m.group(2) != null) { + sb.append("-").append(m.group(2)); + } + + Locale l = new Locale.Builder().setLanguageTag(sb.toString()).build(); + if (l != null) { + Locale.setDefault(l); + logger.debug("Java Locale changed to {}", l); + return true; + } + } + } catch (Exception e) { + System.out.println("Could not get locale from " + localeString); + } + + return false; + } +} diff --git a/src/main/java/org/utplsql/cli/LoggerConfiguration.java b/src/main/java/org/utplsql/cli/LoggerConfiguration.java new file mode 100644 index 0000000..2a83085 --- /dev/null +++ b/src/main/java/org/utplsql/cli/LoggerConfiguration.java @@ -0,0 +1,73 @@ +package org.utplsql.cli; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.encoder.PatternLayoutEncoder; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import org.slf4j.LoggerFactory; + +public class LoggerConfiguration { + + public enum ConfigLevel { + BASIC, NONE, DEBUG + } + + private LoggerConfiguration() { + throw new UnsupportedOperationException(); + } + + static void configure(ConfigLevel level) { + switch (level) { + case BASIC: + configureInfo(); + break; + case NONE: + configureSilent(); + break; + case DEBUG: + configureDebug(); + break; + } + } + + private static void configureSilent() { + setRootLoggerLevel(Level.OFF); + } + + private static void configureInfo() { + setRootLoggerLevel(Level.INFO); + setSingleConsoleAppenderWithLayout("%msg%n"); + } + + private static void configureDebug() { + setRootLoggerLevel(Level.DEBUG); + setSingleConsoleAppenderWithLayout("%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"); + } + + private static void setRootLoggerLevel(Level level) { + Logger root = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + root.setLevel(level); + } + + private static void setSingleConsoleAppenderWithLayout(String patternLayout) { + Logger logger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + + PatternLayoutEncoder ple = new PatternLayoutEncoder(); + ple.setPattern(patternLayout); + + ple.setContext(lc); + ple.start(); + + ConsoleAppender consoleAppender = new ConsoleAppender<>(); + consoleAppender.setEncoder(ple); + consoleAppender.setContext(lc); + consoleAppender.start(); + + logger.detachAndStopAllAppenders(); + logger.setAdditive(false); + logger.addAppender(consoleAppender); + } +} diff --git a/src/main/java/org/utplsql/cli/OracleLibraryChecker.java b/src/main/java/org/utplsql/cli/OracleLibraryChecker.java index 24b02f8..d6d8393 100644 --- a/src/main/java/org/utplsql/cli/OracleLibraryChecker.java +++ b/src/main/java/org/utplsql/cli/OracleLibraryChecker.java @@ -1,25 +1,24 @@ package org.utplsql.cli; -/** Simple class to check whether needed Oracle libraries are on classpath or not +/** + * Simple class to check whether needed Oracle libraries are on classpath or not * * @author pesse */ class OracleLibraryChecker { - private static boolean classExists( String classFullName ){ - try - { + private static boolean classExists(String classFullName) { + try { Class.forName(classFullName); return true; - } - catch ( ClassNotFoundException e ) - { + } catch (ClassNotFoundException e) { return false; } } - /** Checks if OJDBC library is on the classpath by searching for oracle.jdbc.OracleDriver class + /** + * Checks if OJDBC library is on the classpath by searching for oracle.jdbc.OracleDriver class * * @return true or false */ @@ -27,7 +26,8 @@ public static boolean checkOjdbcExists() { return classExists("oracle.jdbc.OracleDriver"); } - /** Checks if Orai18n library is on the classpath by searching for oracle.i18n.text.OraCharset + /** + * Checks if Orai18n library is on the classpath by searching for oracle.i18n.text.OraCharset * * @return true or false */ diff --git a/src/main/java/org/utplsql/cli/ReporterFactoryProvider.java b/src/main/java/org/utplsql/cli/ReporterFactoryProvider.java new file mode 100644 index 0000000..40aea11 --- /dev/null +++ b/src/main/java/org/utplsql/cli/ReporterFactoryProvider.java @@ -0,0 +1,28 @@ +package org.utplsql.cli; + +import org.utplsql.api.compatibility.CompatibilityProxy; +import org.utplsql.api.reporter.CoreReporters; +import org.utplsql.api.reporter.ReporterFactory; +import org.utplsql.cli.reporters.LocalAssetsCoverageHTMLReporter; + +import java.sql.Connection; +import java.sql.SQLException; + +/** + * A simple class to provide a ReporterFactory for the RunCommand + * + * @author pesse + */ +class ReporterFactoryProvider { + + public static ReporterFactory createReporterFactory(CompatibilityProxy proxy) { + ReporterFactory reporterFactory = ReporterFactory.createDefault(proxy); + reporterFactory.registerReporterFactoryMethod(CoreReporters.UT_COVERAGE_HTML_REPORTER.name(), LocalAssetsCoverageHTMLReporter::new, "Will copy all necessary assets to a folder named after the Output-File"); + + return reporterFactory; + } + + public static ReporterFactory createReporterFactory(Connection con) throws SQLException { + return createReporterFactory(new CompatibilityProxy(con)); + } +} diff --git a/src/main/java/org/utplsql/cli/ReporterManager.java b/src/main/java/org/utplsql/cli/ReporterManager.java new file mode 100644 index 0000000..176ae09 --- /dev/null +++ b/src/main/java/org/utplsql/cli/ReporterManager.java @@ -0,0 +1,176 @@ +package org.utplsql.cli; + +import org.utplsql.api.compatibility.CompatibilityProxy; +import org.utplsql.api.reporter.CoreReporters; +import org.utplsql.api.reporter.Reporter; +import org.utplsql.api.reporter.ReporterFactory; +import org.utplsql.cli.config.ReporterConfig; +import org.utplsql.cli.reporters.ReporterOptionsAware; + +import javax.sql.DataSource; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintStream; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.function.Consumer; + +class ReporterManager { + + private final List reporterOptionsList; + private List reporterGatherErrors; + private ExecutorService executorService; + + ReporterManager(ReporterConfig[] reporterConfigs) { + reporterOptionsList = new ArrayList<>(); + if (reporterConfigs != null && reporterConfigs.length > 0) { + loadOptionsFromConfigs(reporterConfigs); + } else { + reporterOptionsList.add(getDefaultReporterOption()); + } + } + + private void loadOptionsFromConfigs(ReporterConfig[] reporterConfigs) { + boolean printToScreen = false; + for (ReporterConfig reporterConfig : reporterConfigs) { + ReporterOptions option = new ReporterOptions( + reporterConfig.getName(), + reporterConfig.getOutput()); + + option.forceOutputToScreen(reporterConfig.isForceToScreen()); + reporterOptionsList.add(option); + + // Check printToScreen validity + if (option.outputToScreen() && printToScreen) { + throw new IllegalArgumentException("You cannot configure more than one reporter to output to screen"); + } + printToScreen = option.outputToScreen(); + } + } + + private ReporterOptions getDefaultReporterOption() { + return new ReporterOptions(CoreReporters.UT_DOCUMENTATION_REPORTER.name()); + } + + private void abortGathering(Exception e) { + addGatherError(e); + executorService.shutdownNow(); + } + + private void addGatherError(Exception e) { + if (reporterGatherErrors == null) { + reporterGatherErrors = new ArrayList<>(); + } + reporterGatherErrors.add(e); + } + + boolean haveGatherErrorsOccured() { + return reporterGatherErrors != null && !reporterGatherErrors.isEmpty(); + } + + List getGatherErrors() { + return reporterGatherErrors; + } + + /** + * Initializes the reporters so we can use the id to gather results + * + * @param conn Active Connection + * @return List of Reporters + * @throws SQLException + */ + List initReporters(Connection conn, ReporterFactory reporterFactory, CompatibilityProxy compatibilityProxy) throws SQLException { + final List reporterList = new ArrayList<>(); + + for (ReporterOptions ro : reporterOptionsList) { + Reporter reporter = reporterFactory.createReporter(ro.getReporterName()); + + if (reporter instanceof ReporterOptionsAware) { + ((ReporterOptionsAware) reporter).setReporterOptions(ro); + } + + reporter.init(conn, compatibilityProxy, reporterFactory); + + ro.setReporterObj(reporter); + reporterList.add(reporter); + } + + return reporterList; + } + + /** + * Starts a separate thread for each Reporter to gather its results + * + * @param executorService + * @param dataSource + */ + void startReporterGatherers(ExecutorService executorService, final DataSource dataSource) { + if (this.executorService != null && !this.executorService.isShutdown()) { + throw new IllegalStateException("There is already a running executor service!"); + } + + this.executorService = executorService; + + reporterOptionsList.forEach((reporterOption) -> executorService.submit( + new GatherReporterOutputTask(dataSource, reporterOption, this::abortGathering) + )); + } + + List getReporterOptionsList() { + return reporterOptionsList; + } + + int getNumberOfReporters() { + return reporterOptionsList.size(); + } + + /** + * Gathers Reporter Output based on ReporterOptions and prints it to a file, System.out or both + */ + private static class GatherReporterOutputTask implements Runnable { + + private final DataSource dataSource; + private final ReporterOptions option; + private final Consumer abortFunction; + + GatherReporterOutputTask(DataSource dataSource, ReporterOptions reporterOption, Consumer abortFunction) { + + if (reporterOption.getReporterObj() == null) { + throw new IllegalArgumentException("Reporter " + reporterOption.getReporterName() + " is not initialized"); + } + + this.dataSource = dataSource; + this.option = reporterOption; + this.abortFunction = abortFunction; + } + + @Override + public void run() { + List printStreams = new ArrayList<>(); + PrintStream fileOutStream = null; + + try (Connection conn = dataSource.getConnection()) { + if (option.outputToScreen()) { + printStreams.add(System.out); + option.getReporterObj().getOutputBuffer().setFetchSize(1); + } + + if (option.outputToFile()) { + fileOutStream = new PrintStream(new FileOutputStream(option.getOutputFileName())); + printStreams.add(fileOutStream); + } + + option.getReporterObj().getOutputBuffer().printAvailable(conn, printStreams); + } catch (SQLException | FileNotFoundException e) { + abortFunction.accept(e); + } finally { + if (fileOutStream != null) { + fileOutStream.close(); + } + } + } + } +} diff --git a/src/main/java/org/utplsql/cli/ReporterOptions.java b/src/main/java/org/utplsql/cli/ReporterOptions.java index a20ae3d..b15dd30 100644 --- a/src/main/java/org/utplsql/cli/ReporterOptions.java +++ b/src/main/java/org/utplsql/cli/ReporterOptions.java @@ -14,15 +14,15 @@ public class ReporterOptions { private Reporter reporterObj = null; - public ReporterOptions(String reporterName, String outputFileName, boolean outputToScreen) { + public ReporterOptions(String reporterName, String outputFileName) { setReporterName(reporterName); setOutputFileName(outputFileName); - this.outputToScreen = outputToScreen; + this.outputToScreen = (outputFileName == null); // If outputFileName is null we assume it should be sent to screen this.forceOutputToScreen = false; } public ReporterOptions(String reporterName) { - this(reporterName, null, true); + this(reporterName, null); } public Reporter getReporterObj() { diff --git a/src/main/java/org/utplsql/cli/ReportersCommand.java b/src/main/java/org/utplsql/cli/ReportersCommand.java new file mode 100644 index 0000000..f7dcddd --- /dev/null +++ b/src/main/java/org/utplsql/cli/ReportersCommand.java @@ -0,0 +1,76 @@ +package org.utplsql.cli; + +import org.utplsql.api.exception.DatabaseNotCompatibleException; +import org.utplsql.api.exception.UtPLSQLNotInstalledException; +import org.utplsql.api.reporter.ReporterFactory; +import org.utplsql.api.reporter.inspect.ReporterInfo; +import org.utplsql.api.reporter.inspect.ReporterInspector; +import org.utplsql.cli.exception.DatabaseConnectionFailed; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import javax.sql.DataSource; +import java.io.PrintStream; +import java.sql.Connection; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +@Command(name = "reporters", description = "prints a list of reporters available in the specified database") +public class ReportersCommand implements ICommand { + + @Parameters(description = UtplsqlPicocliCommand.COMMANDLINE_PARAM_DESCRIPTION, arity = "1") + private String connectionString; + + @Option(names = "-h", usageHelp = true, description = "display this help and exit") + boolean help; + + @Override + public int run() { + LoggerConfiguration.configure(LoggerConfiguration.ConfigLevel.NONE); + + try { + DataSource ds = DataSourceProvider.getDataSource(connectionString, 1); + try (Connection con = ds.getConnection()) { + + ReporterFactory reporterFactory = ReporterFactoryProvider.createReporterFactory(con); + + writeReporters(ReporterInspector.create(reporterFactory, con).getReporterInfos(), System.out); + } + } catch (DatabaseNotCompatibleException | UtPLSQLNotInstalledException | DatabaseConnectionFailed | IllegalArgumentException e) { + System.out.println(e.getMessage()); + return 1; + } catch (Exception e) { + e.printStackTrace(); + return 1; + } + + return 0; + } + + private void writeReporters(List reporterInfos, PrintStream out) { + reporterInfos.stream() + .sorted(Comparator.comparing(ReporterInfo::getName)) + .forEach(info -> writeReporter(info, 4, out)); + } + + private void writeReporter(ReporterInfo info, int padding, PrintStream out) { + + writeReporterName(info, padding, out); + writeReporterDescription(info, padding, out); + + out.println(); + } + + private void writeReporterName(ReporterInfo info, int paddingRight, PrintStream out) { + out.println(info.getName() + ":"); + + } + + private void writeReporterDescription(ReporterInfo info, int paddingLeft, PrintStream out) { + String[] lines = info.getDescription().split("\n"); + String paddingLeftStr = String.format("%1$" + paddingLeft + "s", ""); + Arrays.stream(lines).forEach(line -> out.println(paddingLeftStr + line.trim())); + } +} diff --git a/src/main/java/org/utplsql/cli/RunAction.java b/src/main/java/org/utplsql/cli/RunAction.java new file mode 100644 index 0000000..de1be33 --- /dev/null +++ b/src/main/java/org/utplsql/cli/RunAction.java @@ -0,0 +1,293 @@ +package org.utplsql.cli; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.utplsql.api.*; +import org.utplsql.api.compatibility.CompatibilityProxy; +import org.utplsql.api.compatibility.OptionalFeatures; +import org.utplsql.api.db.DefaultDatabaseInformation; +import org.utplsql.api.exception.DatabaseNotCompatibleException; +import org.utplsql.api.exception.OracleCreateStatmenetStuckException; +import org.utplsql.api.exception.SomeTestsFailedException; +import org.utplsql.api.exception.UtPLSQLNotInstalledException; +import org.utplsql.api.reporter.Reporter; +import org.utplsql.api.reporter.ReporterFactory; +import org.utplsql.cli.config.FileMapperConfig; +import org.utplsql.cli.config.RunCommandConfig; +import org.utplsql.cli.exception.DatabaseConnectionFailed; +import org.utplsql.cli.exception.ReporterTimeoutException; +import org.utplsql.cli.log.StringBlockFormatter; + +import javax.sql.DataSource; +import java.io.File; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.*; + +/** + * Starts a test-runner and gathers the results based on the given Configuration + * + * @author pesse + */ +public class RunAction { + + private static final Logger logger = LoggerFactory.getLogger(RunAction.class); + + private RunCommandConfig config; + + private CompatibilityProxy compatibilityProxy; + private ReporterFactory reporterFactory; + private ReporterManager reporterManager; + + public RunAction(RunCommandConfig config) { + this.config = config; + } + + void init() { + LoggerConfiguration.configure(config.getLogConfigLevel()); + LocaleInitializer.initLocale(); + } + + public RunCommandConfig getConfig() { + return config; + } + + public int doRun() throws OracleCreateStatmenetStuckException { + init(); + outputMainInformation(); + + DataSource dataSource = null; + int returnCode = 0; + try { + + final List reporterList; + + dataSource = DataSourceProvider.getDataSource(config.getConnectString(), getReporterManager().getNumberOfReporters() + 2); + + initDatabase(dataSource); + reporterList = initReporters(dataSource); + + checkForCompatibility(compatibilityProxy.getUtPlsqlVersion()); + + ExecutorService executorService = Executors.newFixedThreadPool(1 + reporterList.size()); + + // Run tests. + Future future = executorService.submit(new RunTestRunnerTask(dataSource, newTestRunner(reporterList), config.isDbmsOutput())); + + // Gather each reporter results on a separate thread. + getReporterManager().startReporterGatherers(executorService, dataSource); + + try { + future.get(config.getTimeoutInMinutes(), TimeUnit.MINUTES); + } catch (TimeoutException e) { + executorService.shutdownNow(); + throw new ReporterTimeoutException(config.getTimeoutInMinutes()); + } catch (ExecutionException e) { + if (e.getCause() instanceof SomeTestsFailedException) { + returnCode = config.getFailureExitCode(); + } else { + executorService.shutdownNow(); + throw e.getCause(); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + throw e; + } finally { + executorService.shutdown(); + if (!executorService.awaitTermination(config.getTimeoutInMinutes(), TimeUnit.MINUTES)) { + throw new ReporterTimeoutException(config.getTimeoutInMinutes()); + } + } + + logger.info("--------------------------------------"); + logger.info("All tests done."); + } catch (OracleCreateStatmenetStuckException e) { + throw e; + } catch (DatabaseNotCompatibleException | UtPLSQLNotInstalledException | DatabaseConnectionFailed | ReporterTimeoutException e) { + System.out.println(e.getMessage()); + returnCode = Cli.DEFAULT_ERROR_CODE; + } catch (Throwable e) { + e.printStackTrace(); + returnCode = Cli.DEFAULT_ERROR_CODE; + } + return returnCode; + } + + public int run() { + for (int i = 1; i < 5; i++) { + try { + return doRun(); + } catch (OracleCreateStatmenetStuckException e) { + logger.warn("WARNING: Caught Oracle stuck during creation of Runner-Statement. Retrying ({})", i); + } + } + + return Cli.DEFAULT_ERROR_CODE; + } + + private void checkForCompatibility(Version utPlSqlVersion) { + if (!OptionalFeatures.FAIL_ON_ERROR.isAvailableFor(utPlSqlVersion) && config.getFailureExitCode() != null) { + System.out.println("You specified option `--failure-exit-code` but your database framework version (" + + utPlSqlVersion.getNormalizedString() + ") is not able to " + + "redirect failureCodes. Please upgrade to a newer version if you want to use that feature."); + } + + if (!OptionalFeatures.RANDOM_EXECUTION_ORDER.isAvailableFor(utPlSqlVersion) && config.isRandomTestOrder()) { + System.out.println("You specified option `-random` but your database framework version (" + + utPlSqlVersion.getNormalizedString() + ") is not able to " + + "redirect failureCodes. Please upgrade to a newer version if you want to use that feature."); + } + + if (!OptionalFeatures.RANDOM_EXECUTION_ORDER.isAvailableFor(utPlSqlVersion) && config.getRandomTestOrderSeed() != null) { + System.out.println("You specified option `-seed` but your database framework version (" + + utPlSqlVersion.getNormalizedString() + ") is not able to " + + "redirect failureCodes. Please upgrade to a newer version if you want to use that feature."); + } + + } + + TestRunner newTestRunner(List reporterList) { + + final File baseDir = new File("").getAbsoluteFile(); + + return new TestRunner() + .addPathList(Arrays.asList(config.getSuitePaths())) + .addReporterList(reporterList) + .sourceMappingOptions(getFileMapperOptionsByParamListItem(config.getSourceMapping(), baseDir)) + .testMappingOptions(getFileMapperOptionsByParamListItem(config.getTestMapping(), baseDir)) + .colorConsole(config.isOutputAnsiColor()) + .failOnErrors(true) + .skipCompatibilityCheck(config.isSkipCompatibilityCheck()) + .includeObjects(Arrays.asList(config.getIncludePackages())) + .excludeObjects(Arrays.asList(config.getExcludePackages())) + .randomTestOrder(config.isRandomTestOrder()) + .randomTestOrderSeed(config.getRandomTestOrderSeed()) + .addTags(Arrays.asList(config.getTags())) + .addCoverageSchemes(Arrays.asList(config.getCoverageSchemes())) + .oraStuckTimeout(config.getOraStuckTimeout()); + } + + private void outputMainInformation() { + + StringBlockFormatter formatter = new StringBlockFormatter("utPLSQL cli"); + formatter.appendLine(CliVersionInfo.getInfo()); + formatter.appendLine(JavaApiVersionInfo.getInfo()); + formatter.appendLine("Java-Version: " + System.getProperty("java.version")); + formatter.appendLine("ORACLE_HOME: " + EnvironmentVariableUtil.getEnvValue("ORACLE_HOME")); + formatter.appendLine("NLS_LANG: " + EnvironmentVariableUtil.getEnvValue("NLS_LANG")); + formatter.appendLine(""); + formatter.appendLine("Thanks for testing!"); + + logger.info(formatter.toString()); + logger.info(""); + } + + private void initDatabase(DataSource dataSource) throws SQLException { + try (Connection conn = dataSource.getConnection()) { + + checkOracleI18nExists(conn); + + compatibilityProxy = checkFrameworkCompatibility(conn); + + logger.info("Successfully connected to database. UtPLSQL core: {}", compatibilityProxy.getVersionDescription()); + logger.info("Oracle-Version: {}", new DefaultDatabaseInformation().getOracleVersion(conn)); + } catch (SQLException e) { + if (e.getErrorCode() == 1017 || e.getErrorCode() == 12514) { + throw new DatabaseConnectionFailed(e); + } else { + throw e; + } + } + } + + /** + * Checks that orai18n library exists and show a warning if not + */ + private void checkOracleI18nExists(Connection con) throws SQLException { + + if (!OracleLibraryChecker.checkOrai18nExists()) { + System.out.println("WARNING: Could not find Oracle i18n driver in classpath. Depending on the database charset " + + "utPLSQL-cli, especially code coverage, might not run properly. It is recommended you download " + + "the i18n driver from the Oracle website and copy it to the 'lib' folder of your utPLSQL-cli installation."); + System.out.println("Download from http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html"); + } + } + + private List initReporters(DataSource dataSource) throws SQLException { + try (Connection conn = dataSource.getConnection()) { + reporterFactory = ReporterFactoryProvider.createReporterFactory(compatibilityProxy); + return getReporterManager().initReporters(conn, reporterFactory, compatibilityProxy); + } + } + + /** + * Returns FileMapperOptions for the first item of a given param list in a baseDir + * + * @param fileMapperConfig + * @param baseDir + * @return FileMapperOptions or null + */ + private FileMapperOptions getFileMapperOptionsByParamListItem(FileMapperConfig fileMapperConfig, File baseDir) { + if (fileMapperConfig != null) { + String sourcePath = fileMapperConfig.getPath(); + + logger.debug("BaseDir: {}", baseDir); + logger.debug("SourcePath: {}", sourcePath); + + List files = new FileWalker().getFileList(baseDir, sourcePath); + + logger.debug("Getting FileMapperOptions - Files: "); + files.forEach(logger::debug); + + FileMapperOptions options = new FileMapperOptions(files); + options.setObjectOwner(fileMapperConfig.getOwner()); + options.setRegexPattern(fileMapperConfig.getRegexExpression()); + options.setTypeSubExpression(fileMapperConfig.getTypeSubexpression()); + options.setOwnerSubExpression(fileMapperConfig.getOwnerSubexpression()); + options.setNameSubExpression(fileMapperConfig.getNameSubexpression()); + + List mappings = new ArrayList<>(); + fileMapperConfig.getTypeMapping().forEach((k, v) -> mappings.add(new KeyValuePair(v, k))); + options.setTypeMappings(mappings); + + return options; + } + + return null; + } + + /** + * Checks whether cli is compatible with the database framework + * + * @param conn Active Connection + * @throws SQLException + */ + private CompatibilityProxy checkFrameworkCompatibility(Connection conn) throws SQLException { + + CompatibilityProxy proxy = new CompatibilityProxy(conn, config.isSkipCompatibilityCheck()); + + if (!config.isSkipCompatibilityCheck()) { + proxy.failOnNotCompatible(); + } else { + System.out.println("Skipping Compatibility check with framework version, expecting the latest version " + + "to be installed in database"); + } + + return proxy; + } + + private ReporterManager getReporterManager() { + if (reporterManager == null) { + reporterManager = new ReporterManager(config.getReporters()); + } + + return reporterManager; + } + + List getReporterOptionsList() { + return getReporterManager().getReporterOptionsList(); + } +} diff --git a/src/main/java/org/utplsql/cli/RunCommand.java b/src/main/java/org/utplsql/cli/RunCommand.java deleted file mode 100644 index 690a453..0000000 --- a/src/main/java/org/utplsql/cli/RunCommand.java +++ /dev/null @@ -1,352 +0,0 @@ -package org.utplsql.cli; - -import com.beust.jcommander.Parameter; -import com.beust.jcommander.Parameters; -import org.utplsql.api.*; -import org.utplsql.api.compatibility.CompatibilityProxy; -import org.utplsql.api.compatibility.OptionalFeatures; -import org.utplsql.api.exception.DatabaseNotCompatibleException; -import org.utplsql.api.exception.SomeTestsFailedException; -import org.utplsql.api.reporter.Reporter; -import org.utplsql.api.reporter.ReporterFactory; -import org.utplsql.cli.exception.DatabaseConnectionFailed; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.PrintStream; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -/** - * Created by vinicius.moreira on 19/04/2017. - * - * @author vinicious moreira - * @author pesse - */ -@Parameters(separators = "=", commandDescription = "run tests") -public class RunCommand { - - @Parameter( - required = true, - converter = ConnectionInfo.ConnectionStringConverter.class, - arity = 1, - description = "/@//[:]/ OR /@ OR /@::") - private List connectionInfoList = new ArrayList<>(); - - @Parameter( - names = {"-p", "--path"}, - description = "run suites/tests by path, format: " + - "-p=[schema|schema:[suite ...][.test]|schema[.suite ...][.test]") - private List testPaths = new ArrayList<>(); - - @Parameter( - names = {"-f", "--format"}, - variableArity = true, - description = "-f=reporter_name [-o=output_file [-s]] - enables specified format reporting to specified " + - "output file (-o) and to screen (-s)") - private List reporterParams = new ArrayList<>(); - - @Parameter( - names = {"-c", "--color"}, - description = "enables printing of test results in colors as defined by ANSICONSOLE standards") - private boolean colorConsole = false; - - @Parameter( - names = {"--failure-exit-code"}, - description = "override the exit code on failure, default = 1") - private int failureExitCode = 1; - - @Parameter( - names = {"-source_path"}, - variableArity = true, - description = "-source_path [-owner=\"owner\" -regex_expression=\"pattern\" " + - "-type_mapping=\"matched_string=TYPE/matched_string=TYPE\" " + - "-owner_subexpression=0 -type_subexpression=0 -name_subexpression=0] - path to project source files") - private List sourcePathParams = new ArrayList<>(); - - @Parameter( - names = {"-test_path"}, - variableArity = true, - description = "-test_path [-regex_expression=\"pattern\" -owner_subexpression=0 -type_subexpression=0 " + - "-name_subexpression=0] - path to project test files") - private List testPathParams = new ArrayList<>(); - - @Parameter( - names = {"-scc", "--skip-compatibility-check"}, - description = "Skips the check for compatibility with database framework. CLI expects the framework to be " + - "most actual. Use this if you use CLI with a development version of utPLSQL-framework") - private boolean skipCompatibilityCheck = false; - - private CompatibilityProxy compatibilityProxy; - - public ConnectionInfo getConnectionInfo() { - return connectionInfoList.get(0); - } - - public List getTestPaths() { - return testPaths; - } - - public int run() throws Exception { - - RunCommandChecker.checkOracleJDBCExists(); - - final ConnectionInfo ci = getConnectionInfo(); - - final List reporterList; - final List reporterOptionsList = getReporterOptionsList(); - final List testPaths = getTestPaths(); - - final File baseDir = new File("").getAbsoluteFile(); - final FileMapperOptions[] sourceMappingOptions = {null}; - final FileMapperOptions[] testMappingOptions = {null}; - - final int[] returnCode = {0}; - - sourceMappingOptions[0] = getFileMapperOptionsByParamListItem(this.sourcePathParams, baseDir); - testMappingOptions[0] = getFileMapperOptionsByParamListItem(this.testPathParams, baseDir); - - // Do the reporters initialization, so we can use the id to run and gather results. - try (Connection conn = ci.getConnection()) { - - // Check if orai18n exists if database version is 11g - RunCommandChecker.checkOracleI18nExists(ci.getOracleDatabaseVersion(conn)); - - // First of all do a compatibility check and fail-fast - compatibilityProxy = checkFrameworkCompatibility(conn); - - reporterList = initReporters(conn, reporterOptionsList); - - } catch (SQLException e) { - if ( e.getErrorCode() == 1017 || e.getErrorCode() == 12514 ) { - throw new DatabaseConnectionFailed(e); - } - else { - throw e; - } - } - - // Output a message if --failureExitCode is set but database framework is not capable of - String msg = RunCommandChecker.getCheckFailOnErrorMessage(failureExitCode, compatibilityProxy.getDatabaseVersion()); - if ( msg != null ) { - System.out.println(msg); - } - - ExecutorService executorService = Executors.newFixedThreadPool(1 + reporterList.size()); - - // Run tests. - executorService.submit(() -> { - try (Connection conn = ci.getConnection()) { - new TestRunner() - .addPathList(testPaths) - .addReporterList(reporterList) - .sourceMappingOptions(sourceMappingOptions[0]) - .testMappingOptions(testMappingOptions[0]) - .colorConsole(this.colorConsole) - .failOnErrors(true) - .skipCompatibilityCheck(skipCompatibilityCheck) - .run(conn); - } catch (SomeTestsFailedException e) { - returnCode[0] = this.failureExitCode; - } catch (SQLException e) { - System.out.println(e.getMessage()); - returnCode[0] = Cli.DEFAULT_ERROR_CODE; - executorService.shutdownNow(); - } - }); - - // Gather each reporter results on a separate thread. - startReporterGatherers(reporterOptionsList, executorService, ci, returnCode); - - executorService.shutdown(); - executorService.awaitTermination(60, TimeUnit.MINUTES); - return returnCode[0]; - } - - /** Initializes the reporters so we can use the id to gather results - * - * @param conn Active Connection - * @param reporterOptionsList - * @return List of Reporters - * @throws SQLException - */ - private List initReporters( Connection conn, List reporterOptionsList ) throws SQLException - { - final List reporterList = new ArrayList<>(); - - for (ReporterOptions ro : reporterOptionsList) { - Reporter reporter = ReporterFactory.createReporter(ro.getReporterName()); - reporter.init(conn); - ro.setReporterObj(reporter); - reporterList.add(reporter); - } - - return reporterList; - } - - /** Starts a separate thread for each Reporter to gather its results - * - * @param reporterOptionsList - * @param executorService - * @param ci - * @param returnCode - */ - private void startReporterGatherers(List reporterOptionsList, ExecutorService executorService, final ConnectionInfo ci, final int[] returnCode) - { - // Gather each reporter results on a separate thread. - for (ReporterOptions ro : reporterOptionsList) { - executorService.submit(() -> { - List printStreams = new ArrayList<>(); - PrintStream fileOutStream = null; - - try (Connection conn = ci.getConnection()) { - if (ro.outputToScreen()) { - printStreams.add(System.out); - } - - if (ro.outputToFile()) { - fileOutStream = new PrintStream(new FileOutputStream(ro.getOutputFileName())); - printStreams.add(fileOutStream); - } - - new OutputBuffer(ro.getReporterObj()).printAvailable(conn, printStreams); - } catch (SQLException | FileNotFoundException e) { - System.out.println(e.getMessage()); - returnCode[0] = Cli.DEFAULT_ERROR_CODE; - executorService.shutdownNow(); - } finally { - if (fileOutStream != null) - fileOutStream.close(); - } - }); - } - } - - /** Returns FileMapperOptions for the first item of a given param list in a baseDir - * - * @param pathParams - * @param baseDir - * @return FileMapperOptions or null - */ - private FileMapperOptions getFileMapperOptionsByParamListItem(List pathParams, File baseDir ) - { - if (!pathParams.isEmpty()) { - String sourcePath = pathParams.get(0); - List files = new FileWalker().getFileList(baseDir, sourcePath); - return getMapperOptions(pathParams, files); - } - - return null; - } - - public List getReporterOptionsList() { - List reporterOptionsList = new ArrayList<>(); - ReporterOptions reporterOptions = null; - - for (String p : reporterParams) { - if (reporterOptions == null || !p.startsWith("-")) { - reporterOptions = new ReporterOptions(p); - reporterOptionsList.add(reporterOptions); - } - else - if (p.startsWith("-o=")) { - reporterOptions.setOutputFileName(p.substring(3)); - } - else - if (p.equals("-s")) { - reporterOptions.forceOutputToScreen(true); - } - } - - // If no reporter parameters were passed, use default reporter. - if (reporterOptionsList.isEmpty()) { - reporterOptionsList.add(new ReporterOptions(CustomTypes.UT_DOCUMENTATION_REPORTER)); - } - - return reporterOptionsList; - } - - /** Checks whether cli is compatible with the database framework - * - * @param conn Active Connection - * @throws SQLException - */ - private CompatibilityProxy checkFrameworkCompatibility(Connection conn) throws SQLException { - - CompatibilityProxy proxy = new CompatibilityProxy(conn, skipCompatibilityCheck); - - if ( !skipCompatibilityCheck ) { - proxy.failOnNotCompatible(); - } - else { - System.out.println("Skipping Compatibility check with framework version, expecting the latest version " + - "to be installed in database"); - } - - return proxy; - } - - public FileMapperOptions getMapperOptions(List mappingParams, List filePaths) { - FileMapperOptions mapperOptions = new FileMapperOptions(filePaths); - - final String OPT_OWNER="-owner="; - final String OPT_REGEX="-regex_expression="; - final String OPT_TYPE_MAPPING="-type_mapping="; - final String OPT_OWNER_SUBEX="-owner_subexpression="; - final String OPT_NAME_SUBEX="-name_subexpression="; - final String OPT_TYPE_SUBEX="-type_subexpression="; - - for (String p : mappingParams) { - if (p.startsWith(OPT_OWNER)) { - mapperOptions.setObjectOwner(p.substring(OPT_OWNER.length())); - } - else - if (p.startsWith(OPT_REGEX)) { - mapperOptions.setRegexPattern(p.substring(OPT_REGEX.length())); - } - else - if (p.startsWith(OPT_TYPE_MAPPING)) { - String typeMappingsParam = p.substring(OPT_TYPE_MAPPING.length()); - - List typeMappings = new ArrayList<>(); - for (String mapping : typeMappingsParam.split("/")) { - String[] values = mapping.split("="); - typeMappings.add(new KeyValuePair(values[0], values[1])); - } - - mapperOptions.setTypeMappings(typeMappings); - } - else - if (p.startsWith(OPT_OWNER_SUBEX)) { - mapperOptions.setOwnerSubExpression(Integer.parseInt(p.substring(OPT_OWNER_SUBEX.length()))); - } - else - if (p.startsWith(OPT_NAME_SUBEX)) { - mapperOptions.setNameSubExpression(Integer.parseInt(p.substring(OPT_NAME_SUBEX.length()))); - } - else - if (p.startsWith(OPT_TYPE_SUBEX)) { - mapperOptions.setTypeSubExpression(Integer.parseInt(p.substring("-type_subexpression=".length()))); - } - } - - return mapperOptions; - } - - /** Returns the version of the database framework if available - * - * @return - */ - public Version getDatabaseVersion() { - if ( compatibilityProxy != null ) - return compatibilityProxy.getDatabaseVersion(); - - return null; - } -} diff --git a/src/main/java/org/utplsql/cli/RunCommandChecker.java b/src/main/java/org/utplsql/cli/RunCommandChecker.java deleted file mode 100644 index e0c1bb2..0000000 --- a/src/main/java/org/utplsql/cli/RunCommandChecker.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.utplsql.cli; - -import org.utplsql.api.Version; -import org.utplsql.api.compatibility.OptionalFeatures; - -/** Helper class to check several circumstances with RunCommand. Might need refactoring. - * - * @author pesse - */ -class RunCommandChecker { - - /** Checks that ojdbc library exists - * - */ - static void checkOracleJDBCExists() - { - if ( !OracleLibraryChecker.checkOjdbcExists() ) - { - System.out.println("Could not find Oracle JDBC driver in classpath. Please download the jar from Oracle website" + - " and copy it to the 'lib' folder of your utPLSQL-cli installation."); - System.out.println("Download from http://www.oracle.com/technetwork/database/features/jdbc/jdbc-ucp-122-3110062.html"); - - throw new RuntimeException("Can't run utPLSQL-cli without Oracle JDBC driver"); - } - } - - /** Checks that orai18n library exists if database is an oracle 11 - * - */ - static void checkOracleI18nExists(String oracleDatabaseVersion ) - { - if ( oracleDatabaseVersion.startsWith("11.") && !OracleLibraryChecker.checkOrai18nExists() ) - { - System.out.println("Warning: Could not find Oracle i18n driver in classpath. Depending on the database charset " + - "utPLSQL-cli might not run properly. It is recommended you download " + - "the i18n driver from the Oracle website and copy it to the 'lib' folder of your utPLSQL-cli installation."); - System.out.println("Download from http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html"); - } - } - - /** Returns a warning message if failureExitCode is specified but database version is too low - * - * @param failureExitCode - * @param databaseVersion - */ - static String getCheckFailOnErrorMessage(int failureExitCode, Version databaseVersion) { - if ( failureExitCode != 1 && !OptionalFeatures.FAIL_ON_ERROR.isAvailableFor(databaseVersion)) { - return "Your database framework version (" + databaseVersion.getNormalizedString() + ") is not able to " + - "redirect failureCodes. Please upgrade to a newer version if you want to use that feature."; - } - - return null; - } -} diff --git a/src/main/java/org/utplsql/cli/RunPicocliCommand.java b/src/main/java/org/utplsql/cli/RunPicocliCommand.java new file mode 100644 index 0000000..2908cf2 --- /dev/null +++ b/src/main/java/org/utplsql/cli/RunPicocliCommand.java @@ -0,0 +1,282 @@ +package org.utplsql.cli; + +import org.utplsql.api.TestRunner; +import org.utplsql.api.reporter.Reporter; +import org.utplsql.cli.config.FileMapperConfig; +import org.utplsql.cli.config.ReporterConfig; +import org.utplsql.cli.config.RunCommandConfig; +import picocli.CommandLine.ArgGroup; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import java.util.*; + +@Command(name = "run", description = "run tests") +public class RunPicocliCommand implements IRunCommand { + + @Parameters(description = UtplsqlPicocliCommand.COMMANDLINE_PARAM_DESCRIPTION) + private String connectionString; + + @Option(names = {"-p", "--path"}, + description = "run suites/tests by path, format: " + + "-p=[schema|schema:[suite ...][.test]|schema[.suite ...][.test]") + private List paths = new ArrayList<>(); + + @Option(names = {"--tags"}, + description = "comma-separated list of tags to run", + split = ",") + private List tags = new ArrayList<>(); + + @Option(names = {"--coverage-schemes"}, + description = "comma-separated list of schemas on which coverage should be gathered", + split = ",") + private List coverageSchemes = new ArrayList<>(); + + + @Option( + names = {"-c", "--color"}, + description = "enables printing of test results in colors as defined by ANSICONSOLE standards") + private boolean colorConsole = false; + + @Option( + names = {"-d", "--debug"}, + description = "Outputs a load of debug information to console") + private boolean logDebug = false; + + @Option( + names = {"-q", "--quiet"}, + description = "Does not output the informational messages normally printed to console") + private boolean logSilent = false; + + @Option( + names = {"--failure-exit-code"}, + description = "override the exit code on failure, default = 1") + private int failureExitCode = 1; + + + @Option( + names = {"-scc", "--skip-compatibility-check"}, + description = "Skips the check for compatibility with database framework. CLI expects the framework to be " + + "most actual. Use this if you use CLI with a development version of utPLSQL-framework") + private boolean skipCompatibilityCheck = false; + + @Option( + names = {"-t", "--timeout"}, + description = "Sets the timeout in minutes after which the cli will abort. Default 60") + private int timeoutInMinutes = 60; + + @Option( + names = {"-include"}, + description = "Comma-separated object list to include in the coverage report. " + + "Format: [schema.]package[,[schema.]package ...]. See coverage reporting options in framework documentation" + ) + private String includeObjects = null; + + @Option( + names = {"-exclude"}, + description = "Comma-separated object list to exclude from the coverage report. " + + "Format: [schema.]package[,[schema.]package ...]. See coverage reporting options in framework documentation" + ) + private String excludeObjects = null; + + + @Option( + names = {"-D", "--dbms_output"}, + description = "Enables DBMS_OUTPUT for the TestRunner (default: DISABLED)" + ) + private boolean enableDbmsOutput = false; + + // RandomTestOrder + @Option( + names = {"-r", "--random-test-order"}, + description = "Enables random order of test executions (default: DISABLED)" + ) + private boolean randomTestOrder = false; + + @Option( + names = {"-seed", "--random-test-order-seed"}, + description = "Sets the seed to use for random test execution order. If set, it sets --random-test-order to true" + ) + private Integer randomTestOrderSeed; + + @ArgGroup(exclusive = false, multiplicity = "0..*") + private List reporters = new ArrayList<>(); + + static class Format { + @Option(names = {"-f", "--format"}, description = "Enables specified format reporting") + String format; + @Option(names = {"-o"}, description = "Outputs format to file") + String outputFile; + @Option(names = {"-s"}, description = "Outputs to screen even when an output file is specified") + boolean outputToScreen = false; + } + + // FileMappings + @ArgGroup(exclusive = false, multiplicity = "0..2") + private List fileMappings; + + static class FileMappingComposite { + + @ArgGroup(exclusive = true, multiplicity = "1") + TestOrSourcePath testOrSourcePath; + + @ArgGroup(exclusive = false, multiplicity = "0..1") + FileMapping mapping; + + static class TestOrSourcePath { + @Option(names = "-source_path", required = true) + String sourcePath; + @Option(names = "-test_path", required = true) + String testPath; + + String getPath() { + return (isSourcePath()) ? sourcePath : testPath; + } + + boolean isSourcePath() { + return sourcePath != null; + } + } + + static class FileMapping { + @Option(names = "-owner") + String owner; + @Option(names = "-regex_expression") + String regexExpression; + @Option(names = "-type_mapping") + String typeMapping; + @Option(names = "-owner_subexpression") + Integer ownerSubExpression; + @Option(names = "-type_subexpression") + Integer typeSubExpression; + @Option(names = "-name_subexpression") + Integer nameSubExpression; + } + + FileMapperConfig toFileMapperConfig() { + if (mapping == null) { + mapping = new FileMapping(); + } + + Map typeMap = new HashMap<>(); + + if (mapping.typeMapping != null && !mapping.typeMapping.isEmpty()) { + for (String keyVal : mapping.typeMapping.split("/")) { + String[] values = keyVal.split("="); + typeMap.put(values[1], values[0]); + } + } + + return new FileMapperConfig( + testOrSourcePath.getPath(), + mapping.owner, + mapping.regexExpression, + typeMap, + mapping.ownerSubExpression, + mapping.nameSubExpression, + mapping.typeSubExpression + ); + } + } + + @Option(names = "-h", usageHelp = true, description = "display this help and exit") + boolean help; + + @Option(names = "--ora-stuck-timeout", description = "Sets a timeout around Reporter creation and retries when not ready after a while. 0 = no timeout.") + Integer oraStuckTimeout = 0; + + private RunAction runAction; + + private String[] splitOrEmpty(String value) { + if (value == null || value.isEmpty()) { + return new String[0]; + } else { + return value.split(","); + } + } + + public RunCommandConfig getRunCommandConfig() { + // Prepare path elements + ArrayList suitePaths = new ArrayList<>(); + for (String pathElem : paths) { + suitePaths.addAll(Arrays.asList(pathElem.split(","))); + } + + // Prepare LogLevelConfig + LoggerConfiguration.ConfigLevel loggerConfigLevel = LoggerConfiguration.ConfigLevel.BASIC; + if (logSilent) { + loggerConfigLevel = LoggerConfiguration.ConfigLevel.NONE; + } else if (logDebug) { + loggerConfigLevel = LoggerConfiguration.ConfigLevel.DEBUG; + } + + // Prepare Reporter configs + List reporterConfigs = new ArrayList<>(); + for (Format format : reporters) { + reporterConfigs.add(new ReporterConfig(format.format, format.outputFile, format.outputToScreen)); + } + + // Prepare TypeMappings + FileMapperConfig sourceFileMapping = null; + FileMapperConfig testFileMapping = null; + if (fileMappings != null) { + for (FileMappingComposite fmc : fileMappings) { + if (fmc.testOrSourcePath.isSourcePath()) { + sourceFileMapping = fmc.toFileMapperConfig(); + } else { + testFileMapping = fmc.toFileMapperConfig(); + } + } + } + + return new RunCommandConfig.Builder() + .connectString(connectionString) + .suitePaths(suitePaths.toArray(new String[0])) + .reporters(reporterConfigs.toArray(new ReporterConfig[0])) + .outputAnsiColor(colorConsole) + .failureExitCode(failureExitCode) + .skipCompatibilityCheck(skipCompatibilityCheck) + .includePackages(splitOrEmpty(includeObjects)) + .excludePackages(splitOrEmpty(excludeObjects)) + .sourceMapping(sourceFileMapping) + .testMapping(testFileMapping) + .logConfigLevel(loggerConfigLevel) + .timeoutInMinutes(timeoutInMinutes) + .dbmsOutput(enableDbmsOutput) + .randomTestOrder(randomTestOrder) + .randomTestOrderSeed(randomTestOrderSeed) + .tags(tags.toArray(new String[0])) + .coverageSchemes(coverageSchemes.toArray(new String[0])) + .oraStuckTimeout(oraStuckTimeout) + .create(); + } + + private RunAction getRunAction() { + if (runAction == null) { + runAction = new RunAction(getRunCommandConfig()); + } + + return runAction; + } + + @Override + public int run() { + return getRunAction().run(); + } + + @Override + public TestRunner newTestRunner(List reporterList) { + return getRunAction().newTestRunner(reporterList); + } + + @Override + public List getReporterOptionsList() { + return getRunAction().getReporterOptionsList(); + } + + @Override + public void initLogger() { + getRunAction().init(); + } +} diff --git a/src/main/java/org/utplsql/cli/RunTestRunnerTask.java b/src/main/java/org/utplsql/cli/RunTestRunnerTask.java new file mode 100644 index 0000000..fc72c3d --- /dev/null +++ b/src/main/java/org/utplsql/cli/RunTestRunnerTask.java @@ -0,0 +1,71 @@ +package org.utplsql.cli; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.utplsql.api.DBHelper; +import org.utplsql.api.TestRunner; +import org.utplsql.api.exception.OracleCreateStatmenetStuckException; +import org.utplsql.api.exception.SomeTestsFailedException; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; + +/** + * Runs the utPLSQL Test-Runner + *

+ * Takes care of its connection. + * In case of an OracleCreateStatementStuckException it will abort the connection, otherwise close it. + * + * @author pesse + */ +public class RunTestRunnerTask implements Callable { + + private static final Logger logger = LoggerFactory.getLogger(RunTestRunnerTask.class); + private DataSource dataSource; + private TestRunner testRunner; + private boolean enableDmbsOutput; + + RunTestRunnerTask(DataSource dataSource, TestRunner testRunner, boolean enableDmbsOutput) { + this.dataSource = dataSource; + this.testRunner = testRunner; + this.enableDmbsOutput = enableDmbsOutput; + } + + @Override + public Boolean call() throws Exception { + Connection conn = null; + try { + conn = dataSource.getConnection(); + if (enableDmbsOutput) DBHelper.enableDBMSOutput(conn); + logger.info("Running tests now."); + logger.info("--------------------------------------"); + testRunner.run(conn); + } catch (SomeTestsFailedException e) { + throw e; + } catch (OracleCreateStatmenetStuckException e) { + try { + conn.abort(Executors.newSingleThreadExecutor()); + conn = null; + } catch (SQLException e1) { + logger.error(e1.getMessage(), e1); + } + throw e; + } catch (SQLException e) { + System.out.println(e.getMessage()); + throw e; + } finally { + if (conn != null) { + try { + if (enableDmbsOutput) DBHelper.disableDBMSOutput(conn); + conn.close(); + } catch (SQLException e) { + logger.error(e.getMessage(), e); + } + } + } + return true; + } +} diff --git a/src/main/java/org/utplsql/cli/UtplsqlPicocliCommand.java b/src/main/java/org/utplsql/cli/UtplsqlPicocliCommand.java new file mode 100644 index 0000000..ca1b647 --- /dev/null +++ b/src/main/java/org/utplsql/cli/UtplsqlPicocliCommand.java @@ -0,0 +1,21 @@ +package org.utplsql.cli; + +import picocli.CommandLine; + +@CommandLine.Command( + name = "utplsql", + description = "utPLSQL cli", + subcommands = { + RunPicocliCommand.class, + VersionInfoCommand.class, + ReportersCommand.class, + CommandLine.HelpCommand.class + }) +public class UtplsqlPicocliCommand { + + public static final String COMMANDLINE_PARAM_DESCRIPTION = "/@//[:]/ OR /@ OR /@::"; + + @CommandLine.Option(names = "-h", usageHelp = true, description = "display this help and exit") + boolean help; + +} diff --git a/src/main/java/org/utplsql/cli/VersionInfoCommand.java b/src/main/java/org/utplsql/cli/VersionInfoCommand.java new file mode 100644 index 0000000..821d0ba --- /dev/null +++ b/src/main/java/org/utplsql/cli/VersionInfoCommand.java @@ -0,0 +1,54 @@ +package org.utplsql.cli; + + +import org.utplsql.api.JavaApiVersionInfo; +import org.utplsql.api.Version; +import org.utplsql.api.db.DefaultDatabaseInformation; +import org.utplsql.api.exception.UtPLSQLNotInstalledException; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +@Command(name = "info", description = "prints version information of cli, java-api and - if connection is given - database utPLSQL framework") +public class VersionInfoCommand implements ICommand { + + @Parameters(description = UtplsqlPicocliCommand.COMMANDLINE_PARAM_DESCRIPTION, arity = "0..1") + private String connectionString; + + @Option(names = "-h", usageHelp = true, description = "display this help and exit") + boolean help; + + public int run() { + LoggerConfiguration.configure(LoggerConfiguration.ConfigLevel.NONE); + + System.out.println(CliVersionInfo.getInfo()); + System.out.println(JavaApiVersionInfo.getInfo()); + + try { + writeUtPlsqlVersion(connectionString); + } catch (SQLException e) { + e.printStackTrace(); + return 1; + } + + return 0; + } + + private void writeUtPlsqlVersion(String connectString) throws SQLException { + if (connectString != null) { + + DataSource dataSource = DataSourceProvider.getDataSource(connectString, 1); + + try (Connection con = dataSource.getConnection()) { + Version v = new DefaultDatabaseInformation().getUtPlsqlFrameworkVersion(con); + System.out.println("utPLSQL " + v.getNormalizedString()); + } catch (UtPLSQLNotInstalledException e) { + System.out.println(e.getMessage()); + } + } + } +} diff --git a/src/main/java/org/utplsql/cli/config/ConnectionConfig.java b/src/main/java/org/utplsql/cli/config/ConnectionConfig.java new file mode 100644 index 0000000..ef0231e --- /dev/null +++ b/src/main/java/org/utplsql/cli/config/ConnectionConfig.java @@ -0,0 +1,18 @@ +package org.utplsql.cli.config; + +import java.beans.ConstructorProperties; + +public class ConnectionConfig { + + private final String connectString; + + @ConstructorProperties({"connectString"}) + public ConnectionConfig(String connectString) { + this.connectString = connectString; + } + + public String getConnectString() { + return connectString; + } + +} diff --git a/src/main/java/org/utplsql/cli/config/FileMapperConfig.java b/src/main/java/org/utplsql/cli/config/FileMapperConfig.java new file mode 100644 index 0000000..f5d4db2 --- /dev/null +++ b/src/main/java/org/utplsql/cli/config/FileMapperConfig.java @@ -0,0 +1,54 @@ +package org.utplsql.cli.config; + +import java.beans.ConstructorProperties; +import java.util.Map; + +public class FileMapperConfig { + + private final String path; + private final String owner; + private final String regexExpression; + private final Map typeMapping; + private final Integer ownerSubexpression; + private final Integer nameSubexpression; + private final Integer typeSubexpression; + + @ConstructorProperties({"path", "owner", "regexExpression", "typeMapping", "ownerSubexpression", "nameSubexpression", "typeSubexpression"}) + public FileMapperConfig(String path, String owner, String regexExpression, Map typeMapping, Integer ownerSubexpression, Integer nameSubexpression, Integer typeSubexpression) { + this.path = path; + this.owner = owner; + this.regexExpression = regexExpression; + this.typeMapping = typeMapping; + this.ownerSubexpression = ownerSubexpression; + this.nameSubexpression = nameSubexpression; + this.typeSubexpression = typeSubexpression; + } + + public String getPath() { + return path; + } + + public String getOwner() { + return owner; + } + + public String getRegexExpression() { + return regexExpression; + } + + public Map getTypeMapping() { + return typeMapping; + } + + public Integer getOwnerSubexpression() { + return ownerSubexpression; + } + + public Integer getNameSubexpression() { + return nameSubexpression; + } + + public Integer getTypeSubexpression() { + return typeSubexpression; + } +} diff --git a/src/main/java/org/utplsql/cli/config/ReporterConfig.java b/src/main/java/org/utplsql/cli/config/ReporterConfig.java new file mode 100644 index 0000000..e3e74ba --- /dev/null +++ b/src/main/java/org/utplsql/cli/config/ReporterConfig.java @@ -0,0 +1,35 @@ +package org.utplsql.cli.config; + +import org.utplsql.api.reporter.CoreReporters; + +import java.beans.ConstructorProperties; + +public class ReporterConfig { + + private final String name; + private final String output; + private boolean forceToScreen = false; + + @ConstructorProperties({"name", "output", "forceToScreen"}) + public ReporterConfig(String name, String output, Boolean forceToScreen) { + if ( name != null ) { + this.name = name; + } else { + this.name = CoreReporters.UT_DOCUMENTATION_REPORTER.name(); + } + this.output = output; + if (forceToScreen != null) this.forceToScreen = forceToScreen; + } + + public String getName() { + return name; + } + + public String getOutput() { + return output; + } + + public boolean isForceToScreen() { + return forceToScreen; + } +} diff --git a/src/main/java/org/utplsql/cli/config/RunCommandConfig.java b/src/main/java/org/utplsql/cli/config/RunCommandConfig.java new file mode 100644 index 0000000..218466b --- /dev/null +++ b/src/main/java/org/utplsql/cli/config/RunCommandConfig.java @@ -0,0 +1,231 @@ +package org.utplsql.cli.config; + + +import org.utplsql.cli.LoggerConfiguration.ConfigLevel; + +import java.beans.ConstructorProperties; + +public class RunCommandConfig extends ConnectionConfig { + + private final String[] suitePaths; + private final ReporterConfig[] reporters; + private boolean outputAnsiColor = false; + private final Integer failureExitCode; + private boolean skipCompatibilityCheck = false; + private final String[] includePackages; + private final String[] excludePackages; + private final FileMapperConfig sourceMapping; + private final FileMapperConfig testMapping; + private final ConfigLevel logConfigLevel; + private final Integer timeoutInMinutes; + private boolean dbmsOutput = false; + private boolean randomTestOrder = false; + private final Integer randomTestOrderSeed; + private final String[] tags; + private final String[] coverageSchemes; + private final Integer oraStuckTimeout; + + @ConstructorProperties({"connectString", "suitePaths", "reporters", "outputAnsiColor", "failureExitCode", "skipCompatibilityCheck", "includePackages", "excludePackages", "sourceMapping", "testMapping", "logConfigLevel", "timeoutInMinutes", "dbmsOutput", "randomTestOrder", "randomTestOrderSeed", "tags", "coverageSchemes", "oraStuckTimeout"}) + public RunCommandConfig(String connectString, String[] suitePaths, ReporterConfig[] reporters, boolean outputAnsiColor, Integer failureExitCode, boolean skipCompatibilityCheck, String[] includePackages, String[] excludePackages, FileMapperConfig sourceMapping, FileMapperConfig testMapping, ConfigLevel logConfigLevel, Integer timeoutInMinutes, boolean dbmsOutput, boolean randomTestOrder, Integer randomTestOrderSeed, String[] tags, String[] coverageSchemes, Integer oraStuckTimeout) { + super(connectString); + this.suitePaths = suitePaths; + this.reporters = reporters; + this.outputAnsiColor = outputAnsiColor; + this.failureExitCode = failureExitCode; + this.skipCompatibilityCheck = skipCompatibilityCheck; + this.includePackages = includePackages; + this.excludePackages = excludePackages; + this.sourceMapping = sourceMapping; + this.testMapping = testMapping; + this.logConfigLevel = logConfigLevel; + this.timeoutInMinutes = timeoutInMinutes; + this.dbmsOutput = dbmsOutput; + this.randomTestOrder = randomTestOrder; + this.randomTestOrderSeed = randomTestOrderSeed; + this.tags = tags; + this.coverageSchemes = coverageSchemes; + this.oraStuckTimeout = oraStuckTimeout; + } + + public String[] getSuitePaths() { + return suitePaths; + } + + public String[] getTags() { + return tags; + } + + public ReporterConfig[] getReporters() { + return reporters; + } + + public boolean isOutputAnsiColor() { + return outputAnsiColor; + } + + public Integer getFailureExitCode() { + return failureExitCode; + } + + public boolean isSkipCompatibilityCheck() { + return skipCompatibilityCheck; + } + + public String[] getIncludePackages() { + return includePackages; + } + + public String[] getExcludePackages() { + return excludePackages; + } + + public FileMapperConfig getSourceMapping() { + return sourceMapping; + } + + public FileMapperConfig getTestMapping() { + return testMapping; + } + + public ConfigLevel getLogConfigLevel() { + return logConfigLevel; + } + + public Integer getTimeoutInMinutes() { + return timeoutInMinutes; + } + + public boolean isDbmsOutput() { + return dbmsOutput; + } + + public boolean isRandomTestOrder() { + return randomTestOrder; + } + + public Integer getRandomTestOrderSeed() { + return randomTestOrderSeed; + } + + public String[] getCoverageSchemes() { + return coverageSchemes; + } + + public Integer getOraStuckTimeout() { return oraStuckTimeout; } + + public static class Builder { + + private String connectString; + private String[] suitePaths = new String[0]; + private ReporterConfig[] reporters; + private boolean outputAnsiColor; + private Integer failureExitCode; + private boolean skipCompatibilityCheck; + private String[] includePackages = new String[0]; + private String[] excludePackages = new String[0]; + private FileMapperConfig sourceMapping; + private FileMapperConfig testMapping; + private ConfigLevel logConfigLevel; + private Integer timeoutInMinutes; + private boolean dbmsOutput; + private boolean randomTestOrder; + private Integer randomTestOrderSeed; + private String[] tags = new String[0]; + private String[] coverageSchemes = new String[0]; + private Integer oraStuckTimeout; + + public Builder connectString(String connectString) { + this.connectString = connectString; + return this; + } + + public Builder suitePaths(String[] suitePaths) { + this.suitePaths = suitePaths; + return this; + } + + public Builder reporters(ReporterConfig[] reporters) { + this.reporters = reporters; + return this; + } + + public Builder outputAnsiColor(boolean outputAnsiColor) { + this.outputAnsiColor = outputAnsiColor; + return this; + } + + public Builder failureExitCode(Integer failureExitCode) { + this.failureExitCode = failureExitCode; + return this; + } + + public Builder skipCompatibilityCheck(boolean skipCompatibilityCheck) { + this.skipCompatibilityCheck = skipCompatibilityCheck; + return this; + } + + public Builder includePackages(String[] includePackages) { + this.includePackages = includePackages; + return this; + } + + public Builder excludePackages(String[] excludePackages) { + this.excludePackages = excludePackages; + return this; + } + + public Builder sourceMapping(FileMapperConfig sourceMapping) { + this.sourceMapping = sourceMapping; + return this; + } + + public Builder testMapping(FileMapperConfig testMapping) { + this.testMapping = testMapping; + return this; + } + + public Builder logConfigLevel(ConfigLevel logConfigLevel) { + this.logConfigLevel = logConfigLevel; + return this; + } + + public Builder timeoutInMinutes(Integer timeoutInMinutes) { + this.timeoutInMinutes = timeoutInMinutes; + return this; + } + + public Builder dbmsOutput(boolean dbmsOutput) { + this.dbmsOutput = dbmsOutput; + return this; + } + + public Builder randomTestOrder(boolean randomTestOrder) { + this.randomTestOrder = randomTestOrder; + return this; + } + + public Builder randomTestOrderSeed(Integer randomTestOrderSeed) { + this.randomTestOrderSeed = randomTestOrderSeed; + return this; + } + + public Builder tags(String[] tags) { + this.tags = tags; + return this; + } + + public Builder coverageSchemes(String[] coverageSchemes) { + this.coverageSchemes = coverageSchemes; + return this; + } + + public Builder oraStuckTimeout(Integer oraStuckTimeout) { + this.oraStuckTimeout = oraStuckTimeout; + return this; + } + + public RunCommandConfig create() { + return new RunCommandConfig(connectString, suitePaths, reporters, outputAnsiColor, failureExitCode, skipCompatibilityCheck, includePackages, excludePackages, sourceMapping, testMapping, logConfigLevel, timeoutInMinutes, dbmsOutput, randomTestOrder, randomTestOrderSeed, tags, coverageSchemes, oraStuckTimeout); + } + } +} diff --git a/src/main/java/org/utplsql/cli/datasource/InitializableOracleDataSource.java b/src/main/java/org/utplsql/cli/datasource/InitializableOracleDataSource.java new file mode 100644 index 0000000..db32b1a --- /dev/null +++ b/src/main/java/org/utplsql/cli/datasource/InitializableOracleDataSource.java @@ -0,0 +1,32 @@ +package org.utplsql.cli.datasource; + +import oracle.jdbc.pool.OracleDataSource; + +import java.sql.CallableStatement; +import java.sql.Connection; +import java.sql.SQLException; + +public class InitializableOracleDataSource extends OracleDataSource { + + private String initSql; + + public InitializableOracleDataSource() throws SQLException { + } + + @Override + public Connection getConnection() throws SQLException { + Connection con = super.getConnection(); + + if ( initSql != null && !initSql.isEmpty() ) { + try (CallableStatement stmt = con.prepareCall(initSql)) { + stmt.execute(); + } + } + + return con; + } + + public void setConnectionInitSql( String sql ) { + this.initSql = sql; + } +} diff --git a/src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java b/src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java new file mode 100644 index 0000000..8f99cf3 --- /dev/null +++ b/src/main/java/org/utplsql/cli/datasource/TestedDataSourceProvider.java @@ -0,0 +1,126 @@ +package org.utplsql.cli.datasource; + +import oracle.jdbc.pool.OracleDataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.utplsql.api.EnvironmentVariableUtil; +import org.utplsql.cli.ConnectionConfig; +import org.utplsql.cli.exception.DatabaseConnectionFailed; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class TestedDataSourceProvider { + + interface ConnectStringPossibility { + String getConnectString(ConnectionConfig config); + + String getMaskedConnectString(ConnectionConfig config); + } + + private static final Logger logger = LoggerFactory.getLogger(TestedDataSourceProvider.class); + private final ConnectionConfig config; + private final List possibilities = new ArrayList<>(); + private final int maxConnections; + + public TestedDataSourceProvider(ConnectionConfig config, int maxConnections) { + this.config = config; + this.maxConnections = maxConnections; + + possibilities.add(new ThickConnectStringPossibility()); + possibilities.add(new ThinConnectStringPossibility()); + } + + public DataSource getDataSource() throws SQLException { + + InitializableOracleDataSource ds = new InitializableOracleDataSource(); + + setInitSqlFrom_NLS_LANG(ds); + setThickOrThinJdbcUrl(ds); + + return ds; + } + + private void setThickOrThinJdbcUrl(OracleDataSource ds) throws SQLException { + List errors = new ArrayList<>(); + Throwable lastException = null; + + ds.setUser(config.getUser()); + ds.setPassword(config.getPassword()); + + for (ConnectStringPossibility possibility : possibilities) { + logger.debug("Try connecting {}", possibility.getMaskedConnectString(config)); + ds.setURL(possibility.getConnectString(config)); + try (Connection ignored = ds.getConnection()) { + logger.info("Use connection string {}", possibility.getMaskedConnectString(config)); + return; + } catch (Error | Exception e) { + errors.add(possibility.getMaskedConnectString(config) + ": " + e.getMessage()); + lastException = e; + } + } + + errors.forEach(System.out::println); + throw new DatabaseConnectionFailed(lastException); + } + + private void setInitSqlFrom_NLS_LANG(InitializableOracleDataSource ds) { + String nls_lang = EnvironmentVariableUtil.getEnvValue("NLS_LANG"); + + if (nls_lang != null) { + Pattern pattern = Pattern.compile("^([a-zA-Z ]+)?_?([a-zA-Z ]+)?\\.?([a-zA-Z0-9]+)?$"); + Matcher matcher = pattern.matcher(nls_lang); + + List sqlCommands = new ArrayList<>(2); + if (matcher.matches()) { + if (matcher.group(1) != null) { + sqlCommands.add(String.format("ALTER SESSION SET NLS_LANGUAGE='%s'", matcher.group(1))); + } + if (matcher.group(2) != null) { + sqlCommands.add(String.format("ALTER SESSION SET NLS_TERRITORY='%s'", matcher.group(2))); + } + + if (sqlCommands.size() > 0) { + StringBuilder sb = new StringBuilder(); + sb.append("BEGIN\n"); + for (String command : sqlCommands) { + sb.append(String.format("EXECUTE IMMEDIATE q'[%s]';\n", command)); + } + sb.append("END;"); + + logger.debug("NLS settings: {}", sb.toString()); + ds.setConnectionInitSql(sb.toString()); + } + } + } + } + + private static class ThickConnectStringPossibility implements ConnectStringPossibility { + @Override + public String getConnectString(ConnectionConfig config) { + return "jdbc:oracle:oci8:@" + config.getConnect(); + } + + @Override + public String getMaskedConnectString(ConnectionConfig config) { + return "jdbc:oracle:oci8:****/****@" + config.getConnect(); + } + } + + private static class ThinConnectStringPossibility implements ConnectStringPossibility { + @Override + public String getConnectString(ConnectionConfig config) { + return "jdbc:oracle:thin:@" + config.getConnect(); + } + + @Override + public String getMaskedConnectString(ConnectionConfig config) { + return "jdbc:oracle:thin:****/****@" + config.getConnect(); + } + } +} diff --git a/src/main/java/org/utplsql/cli/exception/DatabaseConnectionFailed.java b/src/main/java/org/utplsql/cli/exception/DatabaseConnectionFailed.java index 01b03db..fe4bf96 100644 --- a/src/main/java/org/utplsql/cli/exception/DatabaseConnectionFailed.java +++ b/src/main/java/org/utplsql/cli/exception/DatabaseConnectionFailed.java @@ -4,7 +4,7 @@ public class DatabaseConnectionFailed extends SQLException { - public DatabaseConnectionFailed(SQLException cause ) { - super( "Could not establish connection to database. Reason: " + cause.getMessage(), cause); + public DatabaseConnectionFailed(Throwable cause) { + super("Could not establish connection to database. Reason: " + cause.getMessage(), cause); } } diff --git a/src/main/java/org/utplsql/cli/exception/ReporterTimeoutException.java b/src/main/java/org/utplsql/cli/exception/ReporterTimeoutException.java new file mode 100644 index 0000000..5bfdbf9 --- /dev/null +++ b/src/main/java/org/utplsql/cli/exception/ReporterTimeoutException.java @@ -0,0 +1,15 @@ +package org.utplsql.cli.exception; + +public class ReporterTimeoutException extends Exception { + + private final int timeOutInMinutes; + + public ReporterTimeoutException(int timeoutInMinutes) { + super("Timeout while waiting for reporters to finish for " + timeoutInMinutes + " minutes"); + this.timeOutInMinutes = timeoutInMinutes; + } + + public int getTimeOutInMinutes() { + return timeOutInMinutes; + } +} diff --git a/src/main/java/org/utplsql/cli/log/StringBlockFormatter.java b/src/main/java/org/utplsql/cli/log/StringBlockFormatter.java new file mode 100644 index 0000000..370092c --- /dev/null +++ b/src/main/java/org/utplsql/cli/log/StringBlockFormatter.java @@ -0,0 +1,85 @@ +package org.utplsql.cli.log; + +public class StringBlockFormatter { + + private String headline; + private final StringBuilder content = new StringBuilder(); + + public StringBlockFormatter() { + } + + public StringBlockFormatter(String headline) { + setHeadline(headline); + } + + public void setHeadline(String headline) { + this.headline = headline; + } + + public String getHeadline() { + return headline; + } + + public void append(CharSequence seq) { + content.append(seq); + } + + public void appendLine(CharSequence seq) { + content.append(seq).append("\n"); + } + + private int getMaxLength(String[] lines) { + int len = 0; + for (String line : lines) { + if (line.length() > len) { + len = line.length(); + } + } + + if (headline.length() > (len + 6)) { + len = headline.length(); + } + + return len; + } + + public static String getEncapsulatedLine(String line, int maxLength) { + return String.format("# %-" + maxLength + "s #", line); + } + + public static String getEncapsulatedHeadline(String headline, int maxLength) { + String content = new String(new char[maxLength + 8]).replace("\0", "#"); + if (headline == null || headline.isEmpty()) { + return content; + } + + headline = " " + headline + " "; + int start = (int) Math.floor( + (float) content.length() / 2f + - (float) headline.length() / 2f + ); + int end = start + headline.length(); + + return content.substring(0, start) + + headline + + content.substring(end); + } + + public String toString() { + + String[] lines = content.toString().split("\n"); + int maxLen = getMaxLength(lines); + + StringBuilder sb = new StringBuilder(); + + sb.append(getEncapsulatedHeadline(headline, maxLen)).append("\n"); + sb.append(getEncapsulatedLine("", maxLen)).append("\n"); + for (String line : lines) { + sb.append(getEncapsulatedLine(line, maxLen)).append("\n"); + } + sb.append(getEncapsulatedLine("", maxLen)).append("\n"); + sb.append(getEncapsulatedHeadline("", maxLen)); + + return sb.toString(); + } +} diff --git a/src/main/java/org/utplsql/cli/reporters/LocalAssetsCoverageHTMLReporter.java b/src/main/java/org/utplsql/cli/reporters/LocalAssetsCoverageHTMLReporter.java new file mode 100644 index 0000000..602ee7c --- /dev/null +++ b/src/main/java/org/utplsql/cli/reporters/LocalAssetsCoverageHTMLReporter.java @@ -0,0 +1,74 @@ +package org.utplsql.cli.reporters; + +import org.utplsql.api.compatibility.CompatibilityProxy; +import org.utplsql.api.reporter.CoverageHTMLReporter; +import org.utplsql.api.reporter.Reporter; +import org.utplsql.api.reporter.ReporterFactory; +import org.utplsql.cli.ReporterOptions; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * Simple replacement of the CoverageHTMLReporter which writes the necessary assets to a folder + * named after the Output File's name. + * + * @author pesse + */ +public class LocalAssetsCoverageHTMLReporter extends CoverageHTMLReporter implements ReporterOptionsAware { + + private ReporterOptions options; + + public LocalAssetsCoverageHTMLReporter(String selfType, Object[] attributes) { + super(selfType, attributes); + } + + @Override + public Reporter init(Connection con, CompatibilityProxy compatibilityProxy, ReporterFactory reporterFactory) throws SQLException { + super.init(con, compatibilityProxy, reporterFactory); + + if (hasOutputToFile()) { + writeReportAssetsTo(getPhysicalAssetPath()); + } + + return this; + } + + private String getNameOfOutputFile() { + Path outputPath = Paths.get(options.getOutputFileName()); + return outputPath.getName(outputPath.getNameCount() - 1).toString(); + } + + private Path getPhysicalAssetPath() { + Path outputPath = Paths.get(options.getOutputFileName()); + if (outputPath.getNameCount() > 1) { + return outputPath.getParent().resolve(getAssetsPath()); + } else { + return Paths.get(getAssetsPath()); + } + } + + private void setAssetsPathFromOptions() { + if (hasOutputToFile()) { + setAssetsPath(getNameOfOutputFile() + "_assets/"); + } + } + + private boolean hasOutputToFile() { + return (options != null && options.outputToFile()); + } + + @Override + public void setReporterOptions(ReporterOptions options) { + this.options = options; + setAssetsPathFromOptions(); + } + + @Override + protected void setAttributes(Object[] attributes) { + super.setAttributes(attributes); + setAssetsPathFromOptions(); + } +} diff --git a/src/main/java/org/utplsql/cli/reporters/ReporterOptionsAware.java b/src/main/java/org/utplsql/cli/reporters/ReporterOptionsAware.java new file mode 100644 index 0000000..5236715 --- /dev/null +++ b/src/main/java/org/utplsql/cli/reporters/ReporterOptionsAware.java @@ -0,0 +1,12 @@ +package org.utplsql.cli.reporters; + +import org.utplsql.cli.ReporterOptions; + +/** + * Reporters implementing this interface will get their specific ReporterOptions before initialization + * + * @author pesse + */ +public interface ReporterOptionsAware { + void setReporterOptions(ReporterOptions options); +} diff --git a/src/main/resources/utplsql-cli.version b/src/main/resources/utplsql-cli.version new file mode 100644 index 0000000..3fb16e7 --- /dev/null +++ b/src/main/resources/utplsql-cli.version @@ -0,0 +1 @@ +${project.version}.${travisBuildNumber} \ No newline at end of file diff --git a/src/test/java/org/utplsql/cli/AbstractFileOutputTest.java b/src/test/java/org/utplsql/cli/AbstractFileOutputTest.java new file mode 100644 index 0000000..7cafd58 --- /dev/null +++ b/src/test/java/org/utplsql/cli/AbstractFileOutputTest.java @@ -0,0 +1,42 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; + +import java.io.File; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +public abstract class AbstractFileOutputTest { + + private Set tempPaths; + + protected void addTempPath(Path path) { + tempPaths.add(path); + } + + protected boolean tempPathExists( Path path ) { return tempPaths.contains(path); } + + @BeforeEach + public void setupTest() { + tempPaths = new HashSet<>(); + } + + @AfterEach + public void deleteTempFiles() { + tempPaths.forEach(p -> deleteDir(p.toFile())); + } + + void deleteDir(File file) { + if (file.exists()) { + File[] contents = file.listFiles(); + if (contents != null) { + for (File f : contents) { + deleteDir(f); + } + } + file.delete(); + } + } +} diff --git a/src/test/java/org/utplsql/cli/CliHelpTest.java b/src/test/java/org/utplsql/cli/CliHelpTest.java new file mode 100644 index 0000000..baaf25f --- /dev/null +++ b/src/test/java/org/utplsql/cli/CliHelpTest.java @@ -0,0 +1,63 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; +import org.utplsql.cli.util.SystemCapturer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CliHelpTest { + + private SystemCapturer capturer; + + @Test + void show_basic_help_on_help_command() { + capturer = new SystemCapturer.SystemOutCapturer(); + capturer.start(); + TestHelper.runApp("help"); + String output = capturer.stop(); + + assertTrue(output.contains("Usage:")); + } + + @Test + void show_help_for_run_command() { + capturer = new SystemCapturer.SystemOutCapturer(); + capturer.start(); + TestHelper.runApp("run", "-h"); + String output = capturer.stop(); + + assertTrue(output.contains("Usage:")); + } + + @Test + void show_help_for_reporters_command() { + capturer = new SystemCapturer.SystemOutCapturer(); + capturer.start(); + TestHelper.runApp("reporters", "-h"); + String output = capturer.stop(); + + assertTrue(output.contains("Usage:")); + } + + @Test + void show_help_for_info_command() { + capturer = new SystemCapturer.SystemOutCapturer(); + capturer.start(); + TestHelper.runApp("reporters", "-h"); + String output = capturer.stop(); + + assertTrue(output.contains("Usage:")); + } + + @Test + void write_help_to_error_out_on_unknown_command() { + capturer = new SystemCapturer.SystemErrCapturer(); + capturer.start(); + int exitCode = TestHelper.runApp("wtfhappens"); + String output = capturer.stop(); + + assertTrue(output.contains("Usage:")); + assertEquals(1, exitCode); + } +} diff --git a/src/test/java/org/utplsql/cli/CliVersionInfoTest.java b/src/test/java/org/utplsql/cli/CliVersionInfoTest.java new file mode 100644 index 0000000..28db14c --- /dev/null +++ b/src/test/java/org/utplsql/cli/CliVersionInfoTest.java @@ -0,0 +1,13 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CliVersionInfoTest { + + @Test + void getCliVersionInfo() { + assertTrue(CliVersionInfo.getVersion().startsWith("3.1")); + } +} diff --git a/src/test/java/org/utplsql/cli/ConnectionConfigTest.java b/src/test/java/org/utplsql/cli/ConnectionConfigTest.java new file mode 100644 index 0000000..ee1e0b9 --- /dev/null +++ b/src/test/java/org/utplsql/cli/ConnectionConfigTest.java @@ -0,0 +1,57 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class ConnectionConfigTest { + + @Test + void parse() { + ConnectionConfig info = new ConnectionConfig("test/pw@my.local.host/service"); + + assertEquals("test", info.getUser()); + assertEquals("pw", info.getPassword()); + assertEquals("my.local.host/service", info.getConnect()); + assertFalse(info.isSysDba()); + } + + @Test + void parseSysDba() { + ConnectionConfig info = new ConnectionConfig("sys as sysdba/pw@my.local.host/service"); + + assertEquals("sys as sysdba", info.getUser()); + assertEquals("pw", info.getPassword()); + assertEquals("my.local.host/service", info.getConnect()); + assertTrue(info.isSysDba()); + } + @Test + void parseSysOper() { + ConnectionConfig info = new ConnectionConfig("myOperUser as sysoper/passw0rd@my.local.host/service"); + + assertEquals("myOperUser as sysoper", info.getUser()); + assertEquals("passw0rd", info.getPassword()); + assertEquals("my.local.host/service", info.getConnect()); + assertTrue(info.isSysDba()); + } + + @Test + void parseSpecialCharsPW() { + ConnectionConfig info = new ConnectionConfig("test/\"p@ssw0rd=\"@my.local.host/service"); + + assertEquals("test", info.getUser()); + assertEquals("p@ssw0rd=", info.getPassword()); + assertEquals("my.local.host/service", info.getConnect()); + assertFalse(info.isSysDba()); + } + + @Test + void parseSpecialCharsUser() { + ConnectionConfig info = new ConnectionConfig("\"User/Mine@=\"/pw@my.local.host/service"); + + assertEquals("User/Mine@=", info.getUser()); + assertEquals("pw", info.getPassword()); + assertEquals("my.local.host/service", info.getConnect()); + assertFalse(info.isSysDba()); + } +} diff --git a/src/test/java/org/utplsql/cli/ConnectionInfoTest.java b/src/test/java/org/utplsql/cli/ConnectionInfoTest.java deleted file mode 100644 index 2fdc9c4..0000000 --- a/src/test/java/org/utplsql/cli/ConnectionInfoTest.java +++ /dev/null @@ -1,130 +0,0 @@ -package org.utplsql.cli; - -/** - * Created by Vinicius on 21/04/2017. - */ -public class ConnectionInfoTest { - - /** - * Regex pattern to match following connection strings: - * user/pass@host:port/db - * user/pass@host/db - * user/pass@db - */ - -// @Test -// public void valid_Full() { -// try { -// ConnectionInfo ci = new ConnectionInfo() -// .parseConnectionString("my_user/p@ss!@some.server.123-abc.com:3000/db_1.acme.com"); -// Assert.assertEquals("my_user", ci.getUser()); -// Assert.assertEquals("p@ss!", ci.getPassword()); -// Assert.assertEquals("some.server.123-abc.com", ci.getHost()); -// Assert.assertEquals(3000, ci.getPort()); -// Assert.assertEquals("db_1.acme.com", ci.getDatabase()); -// Assert.assertEquals("my_user@some.server.123-abc.com:3000/db_1.acme.com", ci.toString()); -// Assert.assertEquals("jdbc:oracle:thin:@//some.server.123-abc.com:3000/db_1.acme.com", ci.getConnectionUrl()); -// } catch (Exception e) { -// Assert.fail(e.getMessage()); -// } -// } -// -// @Test -// public void valid_WithoutPort() { -// try { -// ConnectionInfo ci = new ConnectionInfo() -// .parseConnectionString("my_user/p@ss!@some.server.123-abc.com/db_1.acme.com"); -// Assert.assertEquals("my_user", ci.getUser()); -// Assert.assertEquals("p@ss!", ci.getPassword()); -// Assert.assertEquals("some.server.123-abc.com", ci.getHost()); -// Assert.assertEquals(1521, ci.getPort()); -// Assert.assertEquals("db_1.acme.com", ci.getDatabase()); -// Assert.assertEquals("my_user@some.server.123-abc.com:1521/db_1.acme.com", ci.toString()); -// Assert.assertEquals("jdbc:oracle:thin:@//some.server.123-abc.com:1521/db_1.acme.com", ci.getConnectionUrl()); -// } catch (Exception e) { -// Assert.fail(e.getMessage()); -// } -// } -// -// @Test -// public void valid_WithoutHostAndPort() { -// try { -// ConnectionInfo ci = new ConnectionInfo() -// .parseConnectionString("my_user/p@ss!@127.0.0.1/db_1.acme.com"); -// Assert.assertEquals("my_user", ci.getUser()); -// Assert.assertEquals("p@ss!", ci.getPassword()); -// Assert.assertEquals("127.0.0.1", ci.getHost()); -// Assert.assertEquals(1521, ci.getPort()); -// Assert.assertEquals("db_1.acme.com", ci.getDatabase()); -// Assert.assertEquals("my_user@127.0.0.1:1521/db_1.acme.com", ci.toString()); -// Assert.assertEquals("jdbc:oracle:thin:@//127.0.0.1:1521/db_1.acme.com", ci.getConnectionUrl()); -// } catch (Exception e) { -// Assert.fail(e.getMessage()); -// } -// } -// -// @Test -// public void invalid_WithoutDatabase_1() { -// try { -// new ConnectionInfo().parseConnectionString("user/pass@"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutDatabase_2() { -// try { -// new ConnectionInfo().parseConnectionString("user/pass"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutDatabase_3() { -// try { -// new ConnectionInfo().parseConnectionString("user/pass@localhost:1521"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutHost() { -// try { -// new ConnectionInfo().parseConnectionString("user/pass@/db"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutPassword() { -// try { -// new ConnectionInfo().parseConnectionString("user/@db"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutUsername() { -// try { -// new ConnectionInfo().parseConnectionString("/pass@db"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutUserPassDb_1() { -// try { -// new ConnectionInfo().parseConnectionString("/@db"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } -// -// @Test -// public void invalid_WithoutUserPass() { -// try { -// new ConnectionInfo().parseConnectionString("@db"); -// Assert.fail(); -// } catch (ParameterException ignored) {} -// } - -} diff --git a/src/test/java/org/utplsql/cli/DataSourceProviderIT.java b/src/test/java/org/utplsql/cli/DataSourceProviderIT.java new file mode 100644 index 0000000..1caaf26 --- /dev/null +++ b/src/test/java/org/utplsql/cli/DataSourceProviderIT.java @@ -0,0 +1,84 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; +import org.utplsql.cli.datasource.TestedDataSourceProvider; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.*; + +class DataSourceProviderIT { + + @Test + void connectToDatabase() throws SQLException { + DataSource dataSource = getDataSource(); + + assertNotNull(dataSource); + } + + //@Test + void connectAsSysdba() throws SQLException { + ConnectionConfig config = new ConnectionConfig("sys as sysdba/oracle@localhost:1522/ORCLPDB1"); + DataSource dataSource = new TestedDataSourceProvider(config, 2).getDataSource(); + + assertNotNull(dataSource); + } + + @Test + void initNlsLang() throws SQLException { + System.setProperty("NLS_LANG", "BRAZILIAN PORTUGUESE_BRAZIL.WE8ISO8859P1"); + DataSource dataSource = getDataSource(); + + assertNotNull(dataSource); + checkNlsSessionParameter(dataSource, "NLS_LANGUAGE", "BRAZILIAN PORTUGUESE"); + checkNlsSessionParameter(dataSource, "NLS_TERRITORY", "BRAZIL"); + } + + @Test + void initPartialNlsLangTerritory() throws SQLException { + System.setProperty("NLS_LANG", "_SOMALIA"); + DataSource dataSource = getDataSource(); + + assertNotNull(dataSource); + checkNlsSessionParameter(dataSource, "NLS_TERRITORY", "SOMALIA"); + } + + @Test + void initPartialNlsLangLanguage() throws SQLException { + System.setProperty("NLS_LANG", "HINDI"); + DataSource dataSource = getDataSource(); + + assertNotNull(dataSource); + checkNlsSessionParameter(dataSource, "NLS_LANGUAGE", "HINDI"); + } + + @Test + void initNlsLangEmpty() throws SQLException { + System.setProperty("NLS_LANG", ""); + DataSource dataSource = getDataSource(); + + assertNotNull(dataSource); + } + + private DataSource getDataSource() throws SQLException { + ConnectionConfig config = new ConnectionConfig(TestHelper.getConnectionString()); + return new TestedDataSourceProvider(config, 2).getDataSource(); + } + + private void checkNlsSessionParameter( DataSource dataSource, String parameterName, String expectedValue ) throws SQLException { + try ( Connection con = dataSource.getConnection() ) { + try (PreparedStatement stmt = con.prepareStatement("select value from nls_session_parameters where parameter = ?")) { + stmt.setString(1, parameterName); + ResultSet rs = stmt.executeQuery(); + if ( rs.next() ) + assertEquals(expectedValue, rs.getString(1)); + else + fail("Could not get NLS Session parameter value for '" + parameterName + "'"); + } + } + } +} diff --git a/src/test/java/org/utplsql/cli/FileWalkerTest.java b/src/test/java/org/utplsql/cli/FileWalkerTest.java index b8e2738..6ec2e1b 100644 --- a/src/test/java/org/utplsql/cli/FileWalkerTest.java +++ b/src/test/java/org/utplsql/cli/FileWalkerTest.java @@ -1,24 +1,25 @@ package org.utplsql.cli; -import org.junit.Assert; -import org.junit.Test; +import org.junit.jupiter.api.Test; import java.io.File; import java.util.Collections; import java.util.List; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + /** * Created by Vinicius on 18/06/2017. */ -public class FileWalkerTest { +class FileWalkerTest { private final File BASE_DIR = new File(new File("").getAbsolutePath(), "assets/demo_project"); @Test - public void fileWalker_Relative() { + void fileWalker_Relative() { List fileList = new FileWalker().getFileList(BASE_DIR, "source"); Collections.sort(fileList); - Assert.assertArrayEquals(new Object[] { + assertArrayEquals(new Object[] { "source/packages/package.pkb".replace('/', File.separatorChar), "source/packages/package.pks".replace('/', File.separatorChar), "source/script.sql".replace('/', File.separatorChar), @@ -27,10 +28,10 @@ public void fileWalker_Relative() { } @Test - public void fileWalker_Absolute() { + void fileWalker_Absolute() { List fileList = new FileWalker().getFileList(BASE_DIR, "source", false); Collections.sort(fileList); - Assert.assertArrayEquals(new Object[] { + assertArrayEquals(new Object[] { BASE_DIR.getAbsolutePath() + "/source/packages/package.pkb".replace('/', File.separatorChar), BASE_DIR.getAbsolutePath() + "/source/packages/package.pks".replace('/', File.separatorChar), BASE_DIR.getAbsolutePath() + "/source/script.sql".replace('/', File.separatorChar), diff --git a/src/test/java/org/utplsql/cli/HelpCommandTest.java b/src/test/java/org/utplsql/cli/HelpCommandTest.java new file mode 100644 index 0000000..fd19bd9 --- /dev/null +++ b/src/test/java/org/utplsql/cli/HelpCommandTest.java @@ -0,0 +1,60 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.utplsql.cli.util.SystemCapturer; + +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class HelpCommandTest { + + + private SystemCapturer capturer; + + @BeforeEach + void setupCaptureSystemOut() { + capturer = new SystemCapturer.SystemOutCapturer(); + } + + @Test + void callHelp() { + + capturer.start(); + int result = TestHelper.runApp("-h"); + String output = capturer.stop(); + + assertEquals(0, result); + assertTrue(output.contains("Usage:")); + } + + @Test + void callRunHelp() { + + capturer.start(); + int result = TestHelper.runApp("run", "-h"); + String output = capturer.stop(); + + assertEquals(0, result); + assertTrue(output.contains("Usage:")); + } + + @Test + void callWithNoArgs() { + + capturer.start(); + int result = TestHelper.runApp(); + String output = capturer.stop(); + + assertEquals(1, result); + assertTrue(output.contains("Usage:")); + } + + @AfterEach + void cleanupCaptureSystemOut() throws IOException { + capturer.stop(); + } +} diff --git a/src/test/java/org/utplsql/cli/PathMapperTest.java b/src/test/java/org/utplsql/cli/PathMapperTest.java new file mode 100644 index 0000000..17798ca --- /dev/null +++ b/src/test/java/org/utplsql/cli/PathMapperTest.java @@ -0,0 +1,26 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; +import org.utplsql.api.TestRunner; + +import java.util.ArrayList; + +public class PathMapperTest { + + @Test + void checkPathMapperOutput() { + IRunCommand runCmd = TestHelper.createRunCommand(TestHelper.getConnectionString(), + "-f=ut_sonar_test_reporter", + "-o=sonar_result.xml", + "-s", + "-d", + "-source_path=src/test/resources/plsql/source", + "-owner=app", + "-regex_expression=\"*\"", + "-type_mapping=\"sql=PACKAGE BODY\"", + "-test_path=src/test/resources/plsql/test" + ); + + TestRunner testRunner = runCmd.newTestRunner(new ArrayList<>());; + } +} diff --git a/src/test/java/org/utplsql/cli/PicocliRunCommandTest.java b/src/test/java/org/utplsql/cli/PicocliRunCommandTest.java new file mode 100644 index 0000000..475ca82 --- /dev/null +++ b/src/test/java/org/utplsql/cli/PicocliRunCommandTest.java @@ -0,0 +1,252 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; +import org.utplsql.cli.config.FileMapperConfig; +import org.utplsql.cli.config.ReporterConfig; +import org.utplsql.cli.config.RunCommandConfig; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.*; + +public class PicocliRunCommandTest { + + private RunCommandConfig parseForConfig( String... args ) throws Exception { + return TestHelper.parseRunConfig(args); + } + + @Test + void runCommandAllArguments() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-p=app.betwnstr,app.basic", + "--tags=tag1,tag.2", + "-d", + "-c", + "-q", + "--failure-exit-code=10", + "-scc", + "-t=60", + "-exclude=app.exclude1,app.exclude2", + "-include=app.include1,app.include2", + "-D", + "-r", + "-seed=123", + "-f=ut_sonar_test_reporter", + "-o=sonar_result.xml", + "-s", + "-source_path=src/test/resources/plsql/source", + "-owner=app", + "-regex_expression=\"*\"", + "-type_mapping=\"sql=PACKAGE BODY\"", + "-owner_subexpression=0", + "-type_subexpression=0", + "-name_subexpression=0", + "-test_path=src/test/resources/plsql/test", + "-owner=test_app", + "-regex_expression=\"test_regex\"", + "-type_mapping=\"tsql=PACKAGE BODY\"", + "-owner_subexpression=1", + "-type_subexpression=2", + "-name_subexpression=3", + "--coverage-schemes=schema1,other_schema"); + + assertNotNull(config.getConnectString()); + assertThat( config.getSuitePaths(), is(new String[]{"app.betwnstr", "app.basic"})); + assertThat( config.getTags(), is(new String[]{"tag1", "tag.2"})); + assertTrue( config.isOutputAnsiColor() ); + assertEquals( LoggerConfiguration.ConfigLevel.NONE, config.getLogConfigLevel()); + assertEquals( 10, config.getFailureExitCode()); + assertTrue( config.isSkipCompatibilityCheck() ); + assertEquals( 60, config.getTimeoutInMinutes()); + assertThat( config.getExcludePackages(), is(new String[]{"app.exclude1", "app.exclude2"})); + assertThat( config.getIncludePackages(), is(new String[]{"app.include1", "app.include2"})); + assertTrue( config.isDbmsOutput() ); + assertTrue( config.isRandomTestOrder() ); + assertEquals( 123, config.getRandomTestOrderSeed() ); + assertNotNull( config.getReporters() ); + assertEquals( 1, config.getReporters().length ); + assertThat( config.getCoverageSchemes(), is(new String[]{"schema1", "other_schema"}) ); + + // Source FileMapping + assertNotNull(config.getSourceMapping()); + assertNotNull(config.getTestMapping()); + } + + @Test + void commaSeparatedPath() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-p=app.betwnstr,app.basic"); + + assertThat( config.getSuitePaths(), is(new String[]{"app.betwnstr", "app.basic"})); + } + + @Test + void multiplePaths() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-p=app.betwnstr", + "-p=app.basic"); + + assertThat( config.getSuitePaths(), is(new String[]{"app.betwnstr", "app.basic"})); + } + + @Test + void combinedOptions() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-cdq"); + + assertTrue( config.isOutputAnsiColor() ); + assertEquals( LoggerConfiguration.ConfigLevel.NONE, config.getLogConfigLevel()); + } + + @Test + void debugLevelOptionsDebug() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-d"); + + assertEquals( LoggerConfiguration.ConfigLevel.DEBUG, config.getLogConfigLevel()); + } + + @Test + void debugLevelOptionsBasic() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString()); + + assertEquals( LoggerConfiguration.ConfigLevel.BASIC, config.getLogConfigLevel()); + } + + @Test + void debugLevelOptionsNone() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-q"); + + assertEquals( LoggerConfiguration.ConfigLevel.NONE, config.getLogConfigLevel()); + } + + @Test + void singleReporter() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-f=ut_documentation_reporter"); + + assertNotNull( config.getReporters() ); + + ReporterConfig reporterConfig = config.getReporters()[0]; + assertEquals("ut_documentation_reporter", reporterConfig.getName()); + assertNull(reporterConfig.getOutput()); + assertFalse(reporterConfig.isForceToScreen()); + } + + @Test + void multipleReporters() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-f=ut_documentation_reporter", + "-o=output1.txt", + "-f=ut_coverage_html", + "-o=output2.html", + "-s"); + + assertNotNull( config.getReporters() ); + + ReporterConfig reporterConfig = config.getReporters()[0]; + assertEquals("ut_documentation_reporter", reporterConfig.getName()); + assertEquals("output1.txt", reporterConfig.getOutput()); + assertFalse(reporterConfig.isForceToScreen()); + + reporterConfig = config.getReporters()[1]; + assertEquals("ut_coverage_html", reporterConfig.getName()); + assertEquals("output2.html", reporterConfig.getOutput()); + assertTrue(reporterConfig.isForceToScreen()); + } + + @Test + void outputWithDefaultReporter() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-o=output1.txt"); + + assertNotNull( config.getReporters() ); + + ReporterConfig reporterConfig = config.getReporters()[0]; + assertEquals("ut_documentation_reporter", reporterConfig.getName().toLowerCase()); + assertEquals("output1.txt", reporterConfig.getOutput()); + assertFalse(reporterConfig.isForceToScreen()); + } + + @Test + void sourceFileMapping() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-source_path=src/test/resources/plsql/source", + "-owner=app", + "-regex_expression=\"[a-Z+]\"", + "-type_mapping=\"sql=PACKAGE BODY/pks=PACKAGE\"", + "-owner_subexpression=0", + "-type_subexpression=1", + "-name_subexpression=3"); + + FileMapperConfig sourceMapperConfig = config.getSourceMapping(); + assertEquals( "src/test/resources/plsql/source", sourceMapperConfig.getPath()); + assertEquals( "app", sourceMapperConfig.getOwner()); + assertEquals( "[a-Z+]", sourceMapperConfig.getRegexExpression()); + assertThat( sourceMapperConfig.getTypeMapping(), hasEntry("PACKAGE BODY", "sql")); + assertThat( sourceMapperConfig.getTypeMapping(), hasEntry("PACKAGE", "pks")); + assertEquals( 0, sourceMapperConfig.getOwnerSubexpression()); + assertEquals( 1, sourceMapperConfig.getTypeSubexpression()); + assertEquals( 3, sourceMapperConfig.getNameSubexpression()); + } + + @Test + void testFileMapping() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-test_path=src/test/resources/plsql/test", + "-owner=test_app", + "-regex_expression=\"test_regex\"", + "-type_mapping=\"tsql=PACKAGE BODY/tsql=FUNCTION\"", + "-owner_subexpression=4", + "-type_subexpression=5", + "-name_subexpression=6"); + + FileMapperConfig testMapperConfig = config.getTestMapping(); + assertEquals( "src/test/resources/plsql/test", testMapperConfig.getPath()); + assertEquals( "test_app", testMapperConfig.getOwner()); + assertEquals( "test_regex", testMapperConfig.getRegexExpression()); + assertThat( testMapperConfig.getTypeMapping(), hasEntry("PACKAGE BODY", "tsql")); + assertThat( testMapperConfig.getTypeMapping(), hasEntry("FUNCTION", "tsql")); + assertEquals( 4, testMapperConfig.getOwnerSubexpression()); + assertEquals( 5, testMapperConfig.getTypeSubexpression()); + assertEquals( 6, testMapperConfig.getNameSubexpression()); + } + + @Test + void testFileMappingWithoutDetails() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "-test_path=src/test/resources/plsql/test"); + + FileMapperConfig testMapperConfig = config.getTestMapping(); + assertEquals( "src/test/resources/plsql/test", testMapperConfig.getPath()); + assertNull( testMapperConfig.getOwner()); + assertNull( testMapperConfig.getRegexExpression()); + assertThat( testMapperConfig.getTypeMapping(), anEmptyMap()); + assertNull( testMapperConfig.getOwnerSubexpression()); + assertNull( testMapperConfig.getTypeSubexpression()); + assertNull( testMapperConfig.getNameSubexpression()); + } + + @Test + void negatedTag() throws Exception { + RunCommandConfig config = parseForConfig("run", + TestHelper.getConnectionString(), + "--tags=\"-dontwantit\""); + + assertThat(config.getTags(), hasItemInArray("-dontwantit") ); + } +} diff --git a/src/test/java/org/utplsql/cli/ReportersCommandIT.java b/src/test/java/org/utplsql/cli/ReportersCommandIT.java new file mode 100644 index 0000000..1bf443f --- /dev/null +++ b/src/test/java/org/utplsql/cli/ReportersCommandIT.java @@ -0,0 +1,16 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ReportersCommandIT { + + @Test + void callReportersWorks() { + + int result = TestHelper.runApp("reporters", TestHelper.getConnectionString()); + + assertEquals(0, result); + } +} diff --git a/src/test/java/org/utplsql/cli/RunCommandArgumentsTest.java b/src/test/java/org/utplsql/cli/RunCommandArgumentsTest.java new file mode 100644 index 0000000..63be6e2 --- /dev/null +++ b/src/test/java/org/utplsql/cli/RunCommandArgumentsTest.java @@ -0,0 +1,81 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.utplsql.api.TestRunner; + +import java.util.ArrayList; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; + +public class RunCommandArgumentsTest { + + @Test + @DisplayName("All arguments are recognized") + public void allArgumentsAreRecognized() { + IRunCommand runCmd = TestHelper.createRunCommand(TestHelper.getConnectionString(), + "-p=app", + "-f=ut_sonar_test_reporter", + "-o=sonar_result.xml", + "-s", + "--tags=tag1,tag2", + "--coverage-schemes=schema1,some_other_schema", + "-d", + "-c", + "--failure-exit-code=10", + "-scc", + "-t=60", + "-exclude=app.betwnstr", + "-include=app.betwnstr", + "-source_path=src/test/resources/plsql/source", + "-owner=app", + "-regex_expression=\"*\"", + "-type_mapping=\"sql=PACKAGE BODY\"", + "-owner_subexpression=0", + "-type_subexpression=0", + "-name_subexpression=0", + "-test_path=src/test/resources/plsql/test", + "-owner=app", + "-regex_expression=\"*\"", + "-type_mapping=\"sql=PACKAGE BODY\"", + "-owner_subexpression=0", + "-type_subexpression=0", + "-name_subexpression=0", + "--ora-stuck-timeout=2" + ); + + TestRunner testRunner = runCmd.newTestRunner(new ArrayList<>()); + } + + @Test + void multiplePaths() { + IRunCommand runCmd = TestHelper.createRunCommand(TestHelper.getConnectionString(), + "-p=app.test_betwnstr,app.test_award_bonus" + ); + + TestRunner testRunner = runCmd.newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().pathList, contains("app.test_betwnstr", "app.test_award_bonus") ); + + } + + @Test + void provideTags() { + IRunCommand runCmd = TestHelper.createRunCommand(TestHelper.getConnectionString(), + "--tags=tag1,tag.2" + ); + + TestRunner testRunner = runCmd.newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().tags, contains("tag1", "tag.2") ); + } + + @Test + void provideCoverageSchemes() { + IRunCommand runCmd = TestHelper.createRunCommand(TestHelper.getConnectionString(), + "--coverage-schemes=schema-1,some_other_schema" + ); + + TestRunner testRunner = runCmd.newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().coverageSchemes, contains("schema-1", "some_other_schema") ); + } +} diff --git a/src/test/java/org/utplsql/cli/RunCommandConfigLevelTest.java b/src/test/java/org/utplsql/cli/RunCommandConfigLevelTest.java new file mode 100644 index 0000000..b677ed2 --- /dev/null +++ b/src/test/java/org/utplsql/cli/RunCommandConfigLevelTest.java @@ -0,0 +1,39 @@ +package org.utplsql.cli; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import org.junit.jupiter.api.Test; +import org.slf4j.LoggerFactory; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class RunCommandConfigLevelTest { + + private Logger getRootLogger() { + return (Logger)LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); + } + + @Test + void defaultIsInfo() { + TestHelper.createRunCommand(TestHelper.getConnectionString()) + .initLogger(); + + assertEquals(Level.INFO, getRootLogger().getLevel()); + } + + @Test + void silentModeSetsLoggerToOff() { + TestHelper.createRunCommand(TestHelper.getConnectionString(), "-q") + .initLogger(); + + assertEquals(Level.OFF, getRootLogger().getLevel()); + } + + @Test + void debugModeSetsLoggerToDebug() { + TestHelper.createRunCommand(TestHelper.getConnectionString(), "-d") + .initLogger(); + + assertEquals(Level.DEBUG, getRootLogger().getLevel()); + } +} diff --git a/src/test/java/org/utplsql/cli/RunCommandConfigParamsArePassedToTestRunnerTest.java b/src/test/java/org/utplsql/cli/RunCommandConfigParamsArePassedToTestRunnerTest.java new file mode 100644 index 0000000..95d146a --- /dev/null +++ b/src/test/java/org/utplsql/cli/RunCommandConfigParamsArePassedToTestRunnerTest.java @@ -0,0 +1,42 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; +import org.utplsql.api.TestRunner; +import org.utplsql.cli.config.RunCommandConfig; + +import java.util.ArrayList; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class RunCommandConfigParamsArePassedToTestRunnerTest { + + @Test + void tags() { + RunCommandConfig config = new RunCommandConfig.Builder() + .tags(new String[]{"tag1", "tag2"}) + .create(); + TestRunner testRunner = new RunAction(config).newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().tags, contains("tag1", "tag2") ); + } + + @Test + void coverageSchemes() { + RunCommandConfig config = new RunCommandConfig.Builder() + .coverageSchemes(new String[]{"schema1", "another_schema", "and-another-one"}) + .create(); + TestRunner testRunner = new RunAction(config).newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().coverageSchemes, contains("schema1", "another_schema", "and-another-one") ); + } + + @Test + void oraStuckTimeout() { + RunCommandConfig config = new RunCommandConfig.Builder() + .oraStuckTimeout(2) + .create(); + TestRunner testRunner = new RunAction(config).newTestRunner(new ArrayList<>()); + assertThat( testRunner.getOptions().oraStuckTimeout, equalTo(2) ); + } +} diff --git a/src/test/java/org/utplsql/cli/RunCommandCoverageReporterIT.java b/src/test/java/org/utplsql/cli/RunCommandCoverageReporterIT.java new file mode 100644 index 0000000..e95dce2 --- /dev/null +++ b/src/test/java/org/utplsql/cli/RunCommandCoverageReporterIT.java @@ -0,0 +1,134 @@ +package org.utplsql.cli; + +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * System tests for Code Coverage Reporter + * + * @author pesse + */ +class RunCommandCoverageReporterIT extends AbstractFileOutputTest { + + private static final Pattern REGEX_COVERAGE_TITLE = Pattern.compile("([a-zA-Z ]+ )?([a-zA-Z0-9\\._]+)<\\/a>"); + + + private String getTempCoverageFileName(int counter) { + + return "tmpCoverage_" + System.currentTimeMillis() + "_" + counter + ".html"; + } + + /** + * Returns a random filename which does not yet exist on the local path + * + * @return + */ + private Path getTempCoverageFilePath() { + + int i = 1; + Path p = Paths.get(getTempCoverageFileName(i)); + + while ((Files.exists(p) || tempPathExists(p)) && i < 100) + p = Paths.get(getTempCoverageFileName(i++)); + + if (i >= 100) + throw new IllegalStateException("Could not get temporary file for coverage output"); + + addTempPath(p); + addTempPath(Paths.get(p.toString()+"_assets")); + + return p; + } + + /** + * Checks Coverage HTML Output if a given packageName is listed + * + * @param content + * @param packageName + * @return + */ + private boolean hasCoverageListed(String content, String packageName) { + Matcher m = REGEX_COVERAGE_TITLE.matcher(content); + + while (m.find()) { + if (packageName.equals(m.group(2))) + return true; + } + + return false; + } + + @Test + void run_CodeCoverageWithIncludeAndExclude() throws Exception { + + Path coveragePath = getTempCoverageFilePath(); + + int result = TestHelper.runApp("run", TestHelper.getConnectionString(), + "-f=ut_coverage_html_reporter", "-o=" + coveragePath, "-s", "-exclude=app.award_bonus,app.betwnstr"); + + + String content = new String(Files.readAllBytes(coveragePath)); + + assertTrue(hasCoverageListed(content, "app.remove_rooms_by_name")); + assertFalse(hasCoverageListed(content, "app.award_bonus")); + assertFalse(hasCoverageListed(content, "app.betwnstr")); + + } + + @Test + void coverageReporterWriteAssetsToOutput() throws Exception { + Path coveragePath = getTempCoverageFilePath(); + Path coverageAssetsPath = Paths.get(coveragePath.toString() + "_assets"); + + TestHelper.runApp("run", TestHelper.getConnectionString(), + "-f=ut_coverage_html_reporter", "-o=" + coveragePath, "-s"); + + // Run twice to test overriding of assets + TestHelper.runApp("run", TestHelper.getConnectionString(), + "-f=ut_coverage_html_reporter", "-o=" + coveragePath, "-s"); + + + // Check application file exists + File applicationJs = coverageAssetsPath.resolve(Paths.get("application.js")).toFile(); + assertTrue(applicationJs.exists()); + + // Check correct script-part in HTML source exists + String content = new String(Files.readAllBytes(coveragePath)); + assertTrue(content.contains("