From e95e0cccccf1d43c73d4e22a79d4c2d970f42cd2 Mon Sep 17 00:00:00 2001 From: Philipp Salvisberg Date: Tue, 30 Jan 2018 21:06:15 +0100 Subject: [PATCH] Part 1 of #5 enable/disable "Run utPLSQL test" context menu item in Connections navigator window --- .../org/utplsql/sqldev/dal/UtplsqlDao.xtend | 151 ++++++++++++++++++ .../menu/UtplsqlContextMenuListener.xtend | 7 +- .../sqldev/menu/UtplsqlController.xtend | 18 ++- .../utplsql/sqldev/model/ut/Annotation.xtend | 29 ++++ .../utplsql/sqldev/parser/UtplsqlParser.xtend | 4 +- .../sqldev/tests/AbstractJdbcTest.xtend | 46 ++++++ .../org/utplsql/sqldev/tests/DalTest.xtend | 138 ++++++++++++++++ .../sqldev/tests/UtplsqlParserTest.xtend | 14 +- sqldev/src/test/resources/test.properties | 10 ++ 9 files changed, 401 insertions(+), 16 deletions(-) create mode 100644 sqldev/src/main/java/org/utplsql/sqldev/dal/UtplsqlDao.xtend create mode 100644 sqldev/src/main/java/org/utplsql/sqldev/model/ut/Annotation.xtend create mode 100644 sqldev/src/test/java/org/utplsql/sqldev/tests/AbstractJdbcTest.xtend create mode 100644 sqldev/src/test/java/org/utplsql/sqldev/tests/DalTest.xtend create mode 100644 sqldev/src/test/resources/test.properties diff --git a/sqldev/src/main/java/org/utplsql/sqldev/dal/UtplsqlDao.xtend b/sqldev/src/main/java/org/utplsql/sqldev/dal/UtplsqlDao.xtend new file mode 100644 index 00000000..6ca460a8 --- /dev/null +++ b/sqldev/src/main/java/org/utplsql/sqldev/dal/UtplsqlDao.xtend @@ -0,0 +1,151 @@ +/* + * Copyright 2018 Philipp Salvisberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.utplsql.sqldev.dal + +import java.sql.Connection +import java.util.List +import org.springframework.dao.EmptyResultDataAccessException +import org.springframework.jdbc.core.BeanPropertyRowMapper +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.jdbc.datasource.SingleConnectionDataSource +import org.utplsql.sqldev.model.ut.Annotation + +class UtplsqlDao { + public static val UTPLSQL_PACKAGE_NAME = "UT" + private var Connection conn + private var JdbcTemplate jdbcTemplate + + new(Connection conn) { + this.conn = conn + this.jdbcTemplate = new JdbcTemplate(new SingleConnectionDataSource(conn, true)) + } + + /** + * Gets the schema name of the utPLSQL installation. + * + * @return utPLSQL schema or null if no utPLSQL is not installed + * @throws DataAccessException if there is a problem + */ + def String getUtplsqlSchema() { + val sql = ''' + SELECT table_owner + FROM all_synonyms + WHERE owner = 'PUBLIC' + AND synonym_name = '«UTPLSQL_PACKAGE_NAME»' + AND table_name = '«UTPLSQL_PACKAGE_NAME»' + ''' + try { + val schema = jdbcTemplate.queryForObject(sql, String) + return schema + } catch (EmptyResultDataAccessException e) { + return null + } + } + + /** + * Checks if the package ut_annotation_manager is installed. + * This package has been introduced with utPLSQL 3.0.4. + * This version is a prerequisite to identify + * utPLSQL unit test procedures. + * + * @return true if ut_annotation_manager package has been found + * @throws DataAccessException if there is a problem + */ + def boolean isUtAnnotationManagerInstalled() { + if (utplsqlSchema !== null) { + val sql = ''' + SELECT count(*) + FROM all_objects + WHERE owner = '«utplsqlSchema»' + AND object_type = 'PACKAGE' + AND object_name = 'UT_ANNOTATION_MANAGER' + ''' + val found = jdbcTemplate.queryForObject(sql, Integer) + return found == 1 + } + return false + } + + /** + * Checks if utPLSQL tests exist + * + * @param owner schema name, mandatory, case-insensitive + * @param objectName name of the package or package body, optional, case-insensitive + * @param subobjectName name of the procedure, optional, case-insensitive + * @return true if at least one test has been found + * @throws DataAccessException if a utPLSQL version less than 3.0.4 is installed or if there are other problems + */ + def boolean containsUtplsqlTest(String owner, String objectName, String subobjectName) { + try { + val sql = ''' + SELECT count( + CASE + WHEN a.name = 'test' + AND (upper(a.subobject_name) = upper(?) OR ? IS NULL) + THEN + 1 + ELSE + NULL + END + ) + FROM TABLE(«utplsqlSchema».ut_annotation_manager.get_annotated_objects(upper(?), 'PACKAGE')) o + CROSS JOIN TABLE(o.annotations) a + WHERE (o.object_name = upper(?) OR ? IS NULL) + AND a.name IN ('test', 'suite') + HAVING count( + CASE + WHEN a.name = 'suite' THEN + 1 + ELSE + NULL + END + ) > 0 + ''' + val found = jdbcTemplate.queryForObject(sql, Integer, #[subobjectName, subobjectName, owner, objectName, objectName]) + return found > 0 + } catch (EmptyResultDataAccessException e) { + return false + } + } + + def boolean containsUtplsqlTest(String owner) { + return containsUtplsqlTest(owner, null, null) + } + + def boolean containsUtplsqlTest(String owner, String objectType) { + return containsUtplsqlTest(owner, objectType, null) + } + + /** + * Gets a list of utPLSQL annotations for a given PL/SQL package specification + * + * @param owner schema name, mandatory, case-insensitive + * @param objectName name of the package or package body, optional, case-insensitive + * @return list of Annotation with name 'suite' or 'test' + * @throws DataAccessException if a utPLSQL version less than 3.0.4 is installed or if there are other problems + */ + def List annotations(String owner, String objectName) { + val sql = ''' + SELECT o.object_owner, o.object_type, o.object_name, a.name, a.text, a.subobject_name + FROM TABLE(«utplsqlSchema».ut_annotation_manager.get_annotated_objects(upper(?), 'PACKAGE')) o + CROSS JOIN TABLE(o.annotations) a + WHERE o.object_name = upper(?) + ''' + val result = jdbcTemplate.query(sql, new BeanPropertyRowMapper(Annotation), #[owner, objectName]) + return result + } + +} \ No newline at end of file diff --git a/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlContextMenuListener.xtend b/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlContextMenuListener.xtend index 00260bb1..f56a5a0b 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlContextMenuListener.xtend +++ b/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlContextMenuListener.xtend @@ -43,16 +43,15 @@ class UtplsqlContextMenuListener implements ContextMenuListener { if (element instanceof DatabaseConnection) { showMenu = true } else if (element instanceof ObjectFolder) { - if (element.objectType == "PACKAGE" || element.objectType == "TYPE") { + if (element.objectType == "PACKAGE") { showMenu = true } } else if (element instanceof PlSqlNode) { - if (element.objectType == "PACKAGE" || element.objectType == "PACKAGE BODY" || - element.objectType == "TYPE" || element.objectType == "TYPE BODY") { + if (element.objectType == "PACKAGE" || element.objectType == "PACKAGE BODY") { showMenu = true } } else if (element instanceof ChildObjectElement) { - if (element.URL.objectType == "PACKAGE" || element.URL.objectType == "TYPE") { + if (element.URL.objectType == "PACKAGE") { showMenu = true } } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlController.xtend b/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlController.xtend index a97eeda3..de2576df 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlController.xtend +++ b/sqldev/src/main/java/org/utplsql/sqldev/menu/UtplsqlController.xtend @@ -23,6 +23,7 @@ import oracle.dbtools.raptor.navigator.impl.ChildObjectElement import oracle.dbtools.raptor.navigator.impl.DatabaseSourceNode import oracle.dbtools.raptor.navigator.impl.ObjectFolder import oracle.dbtools.raptor.navigator.plsql.PlSqlNode +import oracle.dbtools.raptor.utils.Connections import oracle.dbtools.worksheet.editor.Worksheet import oracle.ide.Context import oracle.ide.Ide @@ -30,6 +31,7 @@ import oracle.ide.controller.Controller import oracle.ide.controller.IdeAction import oracle.ide.editor.Editor import org.utplsql.sqldev.UtplsqlWorksheet +import org.utplsql.sqldev.dal.UtplsqlDao import org.utplsql.sqldev.model.URLTools import org.utplsql.sqldev.parser.UtplsqlParser @@ -62,7 +64,21 @@ class UtplsqlController implements Controller { } } else if (view instanceof DBNavigatorWindow) { if (context.selection.length == 1) { - action.enabled = true + val element = context.selection.get(0) + val dao = new UtplsqlDao(Connections.instance.getConnection(context.URL.connectionName)) + if (dao.utAnnotationManagerInstalled) { + if (element instanceof DatabaseConnection) { + action.enabled = dao.containsUtplsqlTest(element.connection.schema) + } else if (element instanceof ObjectFolder) { + action.enabled = dao.containsUtplsqlTest(element.URL.schema) + } else if (element instanceof PlSqlNode) { + action.enabled = dao.containsUtplsqlTest(element.owner, element.objectName) + } else if (element instanceof ChildObjectElement) { + action.enabled = dao.containsUtplsqlTest(element.URL.schema, element.URL.memberObject, element.shortLabel) + } + } else { + action.enabled = true + } } } return true diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/ut/Annotation.xtend b/sqldev/src/main/java/org/utplsql/sqldev/model/ut/Annotation.xtend new file mode 100644 index 00000000..087f2330 --- /dev/null +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/ut/Annotation.xtend @@ -0,0 +1,29 @@ +/* + * Copyright 2018 Philipp Salvisberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.utplsql.sqldev.model.ut + +import org.eclipse.xtend.lib.annotations.Accessors +import org.utplsql.sqldev.model.AbstractModel + +@Accessors +class Annotation extends AbstractModel { + String objectOwner + String objectType + String objectName + String name + String text + String subobjectName +} \ No newline at end of file diff --git a/sqldev/src/main/java/org/utplsql/sqldev/parser/UtplsqlParser.xtend b/sqldev/src/main/java/org/utplsql/sqldev/parser/UtplsqlParser.xtend index 18d7c741..16e45434 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/parser/UtplsqlParser.xtend +++ b/sqldev/src/main/java/org/utplsql/sqldev/parser/UtplsqlParser.xtend @@ -84,7 +84,7 @@ class UtplsqlParser { } private def populateObjects() { - val p = Pattern.compile("(?i)(\\s*)(create(\\s+or\\s+replace)?\\s+(package|type)\\s+(body\\s+)?)([^\\s]+)(\\s+)") + val p = Pattern.compile("(?i)(\\s*)(create(\\s+or\\s+replace)?\\s+(package)\\s+(body\\s+)?)([^\\s]+)(\\s+)") val m = p.matcher(plsqlReduced) while (m.find) { val o = new PlsqlObject @@ -94,7 +94,7 @@ class UtplsqlParser { } } private def populateUnits() { - val p = Pattern.compile("(?i)(\\s*)(function|procedure)(\\s+)([^\\s\\(;]+)") + val p = Pattern.compile("(?i)(\\s*)(procedure)(\\s+)([^\\s\\(;]+)") val m = p.matcher(plsqlReduced) while (m.find) { val u = new Unit diff --git a/sqldev/src/test/java/org/utplsql/sqldev/tests/AbstractJdbcTest.xtend b/sqldev/src/test/java/org/utplsql/sqldev/tests/AbstractJdbcTest.xtend new file mode 100644 index 00000000..b0c55279 --- /dev/null +++ b/sqldev/src/test/java/org/utplsql/sqldev/tests/AbstractJdbcTest.xtend @@ -0,0 +1,46 @@ +/* + * Copyright 2018 Philipp Salvisberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.utplsql.sqldev.tests + +import java.util.Properties +import org.springframework.jdbc.core.JdbcTemplate +import org.springframework.jdbc.datasource.SingleConnectionDataSource + +abstract class AbstractJdbcTest { + protected static var SingleConnectionDataSource dataSource + protected static var JdbcTemplate jdbcTemplate + protected static var SingleConnectionDataSource sysDataSource + protected static var JdbcTemplate sysJdbcTemplate + // static initializer not supported in Xtend, see https://bugs.eclipse.org/bugs/show_bug.cgi?id=429141 + protected static val _staticInitializerForDataSourceAndJdbcTemplate = { + val p = new Properties() + p.load(AbstractJdbcTest.getClass().getResourceAsStream("/test.properties")) + // create dataSource and jdbcTemplate + dataSource = new SingleConnectionDataSource() + dataSource.driverClassName = "oracle.jdbc.OracleDriver" + dataSource.url = '''jdbc:oracle:thin:@«p.getProperty("host")»:«p.getProperty("port")»/«p.getProperty("service")»''' + dataSource.username = p.getProperty("scott_username") + dataSource.password = p.getProperty("scott_password") + jdbcTemplate = new JdbcTemplate(dataSource) + // create dbaDataSource and dbaJdbcTemplate + sysDataSource = new SingleConnectionDataSource() + sysDataSource.driverClassName = "oracle.jdbc.OracleDriver" + sysDataSource.url = '''jdbc:oracle:thin:@«p.getProperty("host")»:«p.getProperty("port")»/«p.getProperty("service")»''' + sysDataSource.username = p.getProperty("sys_username") + sysDataSource.password = p.getProperty("sys_password") + sysJdbcTemplate = new JdbcTemplate(AbstractJdbcTest.sysDataSource) + } +} diff --git a/sqldev/src/test/java/org/utplsql/sqldev/tests/DalTest.xtend b/sqldev/src/test/java/org/utplsql/sqldev/tests/DalTest.xtend new file mode 100644 index 00000000..e23c5c27 --- /dev/null +++ b/sqldev/src/test/java/org/utplsql/sqldev/tests/DalTest.xtend @@ -0,0 +1,138 @@ +/* + * Copyright 2018 Philipp Salvisberg + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package org.utplsql.sqldev.tests + +import java.util.ArrayList +import org.junit.AfterClass +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.Test +import org.springframework.jdbc.BadSqlGrammarException +import org.utplsql.sqldev.dal.UtplsqlDao +import org.utplsql.sqldev.model.ut.Annotation + +class DalTest extends AbstractJdbcTest { + + @BeforeClass + @AfterClass + def static void setupAndTeardown() { + sysJdbcTemplate.execute("CREATE OR REPLACE PUBLIC SYNONYM ut FOR ut3.ut") + try { + jdbcTemplate.execute("DROP PACKAGE junit_utplsql_test_pkg") + } catch (BadSqlGrammarException e) { + // ignore + } + } + + @Test + def void utplsqlSchema() { + sysJdbcTemplate.execute("DROP PUBLIC SYNONYM ut") + val dao = new UtplsqlDao(dataSource.connection) + Assert.assertEquals(null, dao.utplsqlSchema) + setupAndTeardown + Assert.assertEquals("UT3", dao.utplsqlSchema) + } + + @Test + def void isUtAnnotationManagerInstalled() { + val dao = new UtplsqlDao(dataSource.connection) + Assert.assertTrue(dao.utAnnotationManagerInstalled) + } + + @Test + def void containsUtplsqlTest() { + val dao = new UtplsqlDao(dataSource.connection) + Assert.assertFalse(dao.containsUtplsqlTest("scott")) + jdbcTemplate.execute(''' + CREATE OR REPLACE PACKAGE junit_utplsql_test_pkg IS + -- %suite + + -- %test + PROCEDURE t1; + + -- %Test + PROCEDURE t2; + + PROCEDURE t3; + END junit_utplsql_test_pkg; + ''') + Assert.assertTrue(dao.containsUtplsqlTest("scott")) + Assert.assertTrue(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg")) + Assert.assertTrue(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t1")) + Assert.assertTrue(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t2")) + Assert.assertFalse(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t3")) + jdbcTemplate.execute(''' + CREATE OR REPLACE PACKAGE junit_utplsql_test_pkg IS + -- %test + PROCEDURE t1; + + -- %Test + PROCEDURE t2; + + PROCEDURE t3; + END junit_utplsql_test_pkg; + ''') + Assert.assertFalse(dao.containsUtplsqlTest("scott")) + Assert.assertFalse(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg")) + Assert.assertFalse(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t1")) + Assert.assertFalse(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t2")) + Assert.assertFalse(dao.containsUtplsqlTest("scott", "junit_utplsql_test_pkg", "t3")) + jdbcTemplate.execute("DROP PACKAGE junit_utplsql_test_pkg") + } + + @Test + def void annotations() { + val dao = new UtplsqlDao(dataSource.connection) + Assert.assertEquals(new ArrayList, dao.annotations("scott", "junit_utplsql_test_pkg")) + jdbcTemplate.execute(''' + CREATE OR REPLACE PACKAGE junit_utplsql_test_pkg IS + -- %suite + + -- %test + PROCEDURE t1; + + -- %Test + PROCEDURE t2; + + PROCEDURE t3; + END junit_utplsql_test_pkg; + ''') + val effective = dao.annotations("scott", "junit_utplsql_test_pkg") + val expected = new ArrayList + val suite = new Annotation + suite.objectOwner = "SCOTT" + suite.objectType = "PACKAGE" + suite.objectName = "JUNIT_UTPLSQL_TEST_PKG" + suite.name = 'suite' + expected.add(suite) + val t1 = new Annotation + t1.objectOwner = "SCOTT" + t1.objectType = "PACKAGE" + t1.objectName = "JUNIT_UTPLSQL_TEST_PKG" + t1.name = 'test' + t1.subobjectName = 't1' + expected.add(t1) + val t2 = new Annotation + t2.objectOwner = "SCOTT" + t2.objectType = "PACKAGE" + t2.objectName = "JUNIT_UTPLSQL_TEST_PKG" + t2.name = 'test' + t2.subobjectName = 't2' + expected.add(t2) + Assert.assertEquals(expected.toString, effective.toString) + jdbcTemplate.execute("DROP PACKAGE junit_utplsql_test_pkg") + } +} \ No newline at end of file diff --git a/sqldev/src/test/java/org/utplsql/sqldev/tests/UtplsqlParserTest.xtend b/sqldev/src/test/java/org/utplsql/sqldev/tests/UtplsqlParserTest.xtend index e231fa0b..95d03013 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/tests/UtplsqlParserTest.xtend +++ b/sqldev/src/test/java/org/utplsql/sqldev/tests/UtplsqlParserTest.xtend @@ -67,23 +67,19 @@ class UtplsqlParserTest { Assert.assertEquals('''"SCOTT"."PKG"'''.toString, objects.get(1).name) Assert.assertTrue(objects.get(0).position < objects.get(1).position) val units = parser.getUnits - Assert.assertEquals(4, units.size) + Assert.assertEquals(2, units.size) Assert.assertEquals("p", units.get(0).name) - Assert.assertEquals("f", units.get(1).name) - Assert.assertEquals('''"P"'''.toString, units.get(2).name) - Assert.assertEquals('''"F"'''.toString, units.get(3).name) + Assert.assertEquals('''"P"'''.toString, units.get(1).name) Assert.assertTrue(units.get(0).position < units.get(1).position) - Assert.assertTrue(units.get(1).position < units.get(2).position) - Assert.assertTrue(units.get(2).position < units.get(3).position) Assert.assertEquals("", parser.getPathAt(0)) Assert.assertEquals("", parser.getPathAt(3,6)) Assert.assertEquals("pkg", parser.getPathAt(4,1)) Assert.assertEquals("pkg.p", parser.getPathAt(10,33)) - Assert.assertEquals("pkg.f", parser.getPathAt(13,1)) + Assert.assertEquals("pkg.p", parser.getPathAt(13,1)) Assert.assertEquals("SCOTT.PKG.P", parser.getPathAt(19,1)) Assert.assertEquals("SCOTT.PKG.P", parser.getPathAt(22,9)) - Assert.assertEquals("SCOTT.PKG.F", parser.getPathAt(22,10)) - Assert.assertEquals("SCOTT.PKG.F", parser.getPathAt(29,1)) + Assert.assertEquals("SCOTT.PKG.P", parser.getPathAt(22,10)) + Assert.assertEquals("SCOTT.PKG.P", parser.getPathAt(29,1)) } @Test diff --git a/sqldev/src/test/resources/test.properties b/sqldev/src/test/resources/test.properties new file mode 100644 index 00000000..3b8b4b51 --- /dev/null +++ b/sqldev/src/test/resources/test.properties @@ -0,0 +1,10 @@ +# properties to connect to Oracle Database using JDBC thin driver +host=localhost +port=1521 +service=odb.docker + +# oracle users +scott_username=scott +scott_password=tiger +sys_username=sys as sysdba +sys_password=oracle