From e0ec8ebd5a08ea367455795cd71e0f622843a80e Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 5 Aug 2014 09:49:28 -0700 Subject: [PATCH 01/21] Persist num rows per page in cookie The default number of rows per page in the result analysis table is still 10, but if the user changes that their choice will be remembered for next time. --- grails/web-app/js/cuanto/analysisTable.js | 35 +++++++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/grails/web-app/js/cuanto/analysisTable.js b/grails/web-app/js/cuanto/analysisTable.js index 1de8e0d..11f5a89 100644 --- a/grails/web-app/js/cuanto/analysisTable.js +++ b/grails/web-app/js/cuanto/analysisTable.js @@ -25,6 +25,9 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN var analysisCookieName = "cuantoAnalysis"; var prefTcFormat = "tcFormat"; var prefHiddenColumns = "hiddenColumns"; + var prefRowsPerPage = "rowsPerPage"; + + var defaultRowsPerPage = 10; var dataTable; var analysisDialog; @@ -79,6 +82,7 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN onSearchTermChange(null); dataTable = new YAHOO.widget.DataTable("trDetailsTable", getDataTableColumnDefs(), getAnalysisDataSource(), getDataTableConfig()); + dataTable.get('paginator').subscribe('rowsPerPageChange', onRowsPerPageChange); var hiddenCols = getHiddenColumns(); @@ -186,7 +190,7 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN width: tableWidth + "px", renderLoopSize:10, generateRequest: buildOutcomeQueryString, - paginator: getDataTablePaginator(0, Number.MAX_VALUE, 0, 10), + paginator: getDataTablePaginator(0, Number.MAX_VALUE, 0, getRowsPerPage(defaultRowsPerPage)), sortedBy: {key:"testCase", dir:YAHOO.widget.DataTable.CLASS_ASC}, dynamicData: true }; @@ -412,13 +416,14 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN function getDefaultTableState() { - var tcFormat = YAHOO.util.Cookie.getSub(analysisCookieName, prefTcFormat); + var tcFormat = getSubCookie(prefTcFormat); if (tcFormat) { $('#tcFormat').val(tcFormat); } else { setTcFormatPref(); } - return "format=json&offset=0&max=10&order=asc&sort=testCase&filter=" + getCurrentFilter() + + return "format=json&offset=0&max=" + getRowsPerPage(defaultRowsPerPage) + + "&order=asc&sort=testCase&filter=" + getCurrentFilter() + "&tcFormat=" + tcFormat + "&rand=" + new Date().getTime(); } @@ -830,19 +835,37 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN function onTcFormatChange(e) { - YAHOO.util.Cookie.setSub(analysisCookieName, prefTcFormat, getCurrentTcFormat(), {path: "/", expires: new Date().getDate() + 30}); setTcFormatPref(); onTableStateChange(e); } function setTcFormatPref() { var tcFormat = getCurrentTcFormat(); + setSubCookie(prefTcFormat, tcFormat); + return tcFormat; + } + + function onRowsPerPageChange(e) { + setSubCookie("rowsPerPage", e.newValue); + } + + function getRowsPerPage(defaultVal) { + var rowsPerPage = getSubCookie("rowsPerPage"); + if (!rowsPerPage) { + rowsPerPage = defaultVal; + } + return rowsPerPage; + } + + function setSubCookie(name, value) { var expDate = new Date(); expDate.setDate(expDate.getDate() + 30); - YAHOO.util.Cookie.setSub(analysisCookieName, prefTcFormat, tcFormat, {path: "/", expires: expDate}); - return tcFormat; + YAHOO.util.Cookie.setSub(analysisCookieName, name, value, {path: "/", expires: expDate}) } + function getSubCookie(name) { + return YAHOO.util.Cookie.getSub(analysisCookieName, name); + } function onTableStateChange(e) { var newRequest = generateNewRequest(); From 7c0ca64475ec816fc5a8f7454a7b4ee6a6c1fa97 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 5 Aug 2014 09:51:01 -0700 Subject: [PATCH 02/21] Increase analysis table view pref cookie exp to a year. --- grails/web-app/js/cuanto/analysisTable.js | 2 +- grails/web-app/js/cuanto/columnDialog.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grails/web-app/js/cuanto/analysisTable.js b/grails/web-app/js/cuanto/analysisTable.js index 11f5a89..8fc616c 100644 --- a/grails/web-app/js/cuanto/analysisTable.js +++ b/grails/web-app/js/cuanto/analysisTable.js @@ -859,7 +859,7 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN function setSubCookie(name, value) { var expDate = new Date(); - expDate.setDate(expDate.getDate() + 30); + expDate.setDate(expDate.getDate() + 365); YAHOO.util.Cookie.setSub(analysisCookieName, name, value, {path: "/", expires: expDate}) } diff --git a/grails/web-app/js/cuanto/columnDialog.js b/grails/web-app/js/cuanto/columnDialog.js index 5822aad..4a974ee 100644 --- a/grails/web-app/js/cuanto/columnDialog.js +++ b/grails/web-app/js/cuanto/columnDialog.js @@ -56,7 +56,7 @@ YAHOO.cuanto.ColumnDialog = function (datatable, overlayManager, subCookieName, function setAnalysisColumnPref() { var expDate = new Date(); - expDate.setDate(expDate.getDate() + 30); + expDate.setDate(expDate.getDate() + 365); var colHidden = []; $.each(datatable.getColumnSet().flat, function(idx, column) { From a38ddec967f34a75bf2c7c928fa18e5dfa84d11a Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 5 Aug 2014 11:37:05 -0700 Subject: [PATCH 03/21] Fill window width with results Resize the result table on window resize and fill the window width. --- grails/grails-app/views/testRun/results.gsp | 18 ++++++++++++++---- grails/web-app/css/analysis.css | 4 ++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/grails/grails-app/views/testRun/results.gsp b/grails/grails-app/views/testRun/results.gsp index 98e99fd..988da75 100644 --- a/grails/grails-app/views/testRun/results.gsp +++ b/grails/grails-app/views/testRun/results.gsp @@ -74,14 +74,24 @@ along with this program. If not, see . YAHOO.util.Event.onDOMReady(function () { - var newWidth = $(window).width() * .95; - $('#tabContainer').width(newWidth); - tabView = new YAHOO.widget.TabView("tabContainer"); + + var delay; + var onWindowResize = function() { + clearTimeout(delay); + delay = setTimeout(resizeContent, 200); + } + function resizeContent() { + var newWidth = $(window).width() - 30; + $('#tabContainer').width(newWidth); + } + YAHOO.util.Event.addListener(window, "resize", onWindowResize); + + resizeContent(); + tabView = new YAHOO.widget.TabView("tabContainer"); new YAHOO.cuanto.SummaryTab(); new YAHOO.cuanto.AnalysisTable(${testResultList}, ${analysisStateList}); new YAHOO.cuanto.GroupedOutput(); - }); diff --git a/grails/web-app/css/analysis.css b/grails/web-app/css/analysis.css index d371a8c..7e96193 100644 --- a/grails/web-app/css/analysis.css +++ b/grails/web-app/css/analysis.css @@ -184,6 +184,10 @@ overflow: auto; } +#trDetailsTable > table { + width: 100%; +} + .yui-skin-sam tr.yui-dt-selected td a { color: white; } From 95cf59acaeb485d973f05674f379f02f1ec1b0e6 Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 7 Aug 2014 16:12:00 -0700 Subject: [PATCH 04/21] Don't allow outcome property sorting Since the backend doesn't support it. --- grails/web-app/js/cuanto/analysisTable.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails/web-app/js/cuanto/analysisTable.js b/grails/web-app/js/cuanto/analysisTable.js index 8fc616c..61a685a 100644 --- a/grails/web-app/js/cuanto/analysisTable.js +++ b/grails/web-app/js/cuanto/analysisTable.js @@ -248,7 +248,7 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN return name.toLowerCase() == prop.toLowerCase(); }); if (matchingNames.length == 0) { - var col = new YAHOO.widget.Column({key: prop, label: prop, resizeable: true, width: 100, sortable:true, + var col = new YAHOO.widget.Column({key: prop, label: prop, resizeable: true, width: 100, sortable:false, formatter: propertyFormatter}); var hiddenCols = getHiddenColumns(); dataTable.insertColumn(col); From afaa925aa82344b9947da6ae6c246b3dbdf7cbb1 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 08:32:14 -0700 Subject: [PATCH 05/21] Fix Ivy download url. --- grails/build.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails/build.xml b/grails/build.xml index bc256f9..8593cd2 100644 --- a/grails/build.xml +++ b/grails/build.xml @@ -16,7 +16,7 @@ - From 5fbc24f6d810c58bfe5d18020ce80b98dcc77e7b Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 08:39:39 -0700 Subject: [PATCH 06/21] Allow search by property value without name --- .../TestOutcomeHasAllPropertiesQueryModule.groovy | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/grails/grails-app/utils/cuanto/queryprocessor/TestOutcomeHasAllPropertiesQueryModule.groovy b/grails/grails-app/utils/cuanto/queryprocessor/TestOutcomeHasAllPropertiesQueryModule.groovy index a8aac7e..dbe95e0 100644 --- a/grails/grails-app/utils/cuanto/queryprocessor/TestOutcomeHasAllPropertiesQueryModule.groovy +++ b/grails/grails-app/utils/cuanto/queryprocessor/TestOutcomeHasAllPropertiesQueryModule.groovy @@ -36,8 +36,11 @@ class TestOutcomeHasAllPropertiesQueryModule implements QueryModule { queryFilter.hasAllTestOutcomeProperties.eachWithIndex { prop, indx -> fromClauses << " left join t.testProperties prop_${indx} " - whereClauses << " upper(prop_${indx}.name) = ? and upper(prop_${indx}.value) like ? " - qryArgs << prop.name.toUpperCase() + if (prop.name.trim()) { + whereClauses << " upper(prop_${indx}.name) = ? " + qryArgs << prop.name.toUpperCase() + } + whereClauses << " upper(prop_${indx}.value) like ? " qryArgs << "%${prop.value.toUpperCase()}%" } From 9e2f28d3ab28b962f13847e17f31dd9cfe2589f9 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 08:48:53 -0700 Subject: [PATCH 07/21] Filter export based on view The export feature was only allowing for dumping the results of an entire test run. This will pass the query parameters that are being used to create the current view to the export function so that the data exported will match what the user has filtered. --- .../cuanto/TestRunController.groovy | 14 +++- .../services/cuanto/TestOutcomeService.groovy | 81 ++++++++++++++++--- grails/grails-app/views/testRun/_header.gsp | 5 +- grails/grails-app/views/testRun/export.gsp | 8 +- grails/web-app/js/cuanto/analysisTable.js | 34 ++++++++ 5 files changed, 124 insertions(+), 18 deletions(-) diff --git a/grails/grails-app/controllers/cuanto/TestRunController.groovy b/grails/grails-app/controllers/cuanto/TestRunController.groovy index c909063..ba475b5 100644 --- a/grails/grails-app/controllers/cuanto/TestRunController.groovy +++ b/grails/grails-app/controllers/cuanto/TestRunController.groovy @@ -152,6 +152,15 @@ class TestRunController { def outcomes = { Map results = testOutcomeService.getTestOutcomeQueryResultsForParams(params) + def columns + if (params.columns) { + // I don't know why occasionally the columns come in as an array instead of String + if (params.columns.class.isArray() && !params.columns.isEmpty()) { + params.columns = params.columns[0] + } + columns = params.columns.tokenize(',').collect{ it.tokenize('|') } + } + withFormat { json { def formatter = testOutcomeService.getTestCaseFormatter(params.tcFormat) @@ -175,11 +184,11 @@ class TestRunController { } csv { response.contentType = "text/csv" - render testOutcomeService.getDelimitedTextForTestOutcomes(results?.testOutcomes, ",") + render testOutcomeService.getDelimitedTextForTestOutcomes(results?.testOutcomes, columns, ",") } tsv { response.contentType = 'text/tab-separated-values' - render testOutcomeService.getDelimitedTextForTestOutcomes(results?.testOutcomes, "\t") + render testOutcomeService.getDelimitedTextForTestOutcomes(results?.testOutcomes, columns, "\t") } } } @@ -511,6 +520,7 @@ class TestRunController { def export = { + println("TestRunController.export( " + params + " )" ) [testRun: TestRun.get(params.id)] } diff --git a/grails/grails-app/services/cuanto/TestOutcomeService.groovy b/grails/grails-app/services/cuanto/TestOutcomeService.groovy index 8926b4c..a2e168b 100644 --- a/grails/grails-app/services/cuanto/TestOutcomeService.groovy +++ b/grails/grails-app/services/cuanto/TestOutcomeService.groovy @@ -447,22 +447,83 @@ class TestOutcomeService { } - String getDelimitedTextForTestOutcomes(List outcomes, String delimiter) { + String getDelimitedTextForTestOutcomes(List outcomes, List columns, String delimiter) { StringBuffer buff = new StringBuffer() String d = delimiter - buff << "Test Outcome ID${d}Test Result${d}Analysis State${d}Started At${d}Finished At${d}Duration${d}Bug Title${d}Bug URL${d}Note\n" + def standardColumns = ["testCase", "parameters", "tags", "result", "streak", "successRate", "analysisState", + "startedAt", "finishedAt", "duration", "bug", "owner", "note", "testOutput", "links"] + def columnNames = ["Name", "Test Result", "Analysis State", "Started At", "Finished At", "Duration", + "Bug", "Note"] + def columnFields = ["testCase", "result", "analysisState", "startedAt", "finishedAt", "duration", + "bug", "note"] + def propertyFields = [] + + if (columns) { + columnNames = [] + columnFields = [] + columns.each { column -> + columnNames << column[0] + columnFields << column[1] + if (!standardColumns.contains(column[1])) { + propertyFields << column[0] + } + } + } + + buff << "Test Outcome ID${d}" + columnNames.each { name -> + buff << name << d + } + buff.setLength(buff.length() - 1) + buff << '\n' outcomes.each { outcome -> def renderList = [] renderList << outcome.id - renderList << outcome.testResult.name - renderList << outcome.analysisState?.name - renderList << outcome.startedAt - renderList << outcome.finishedAt - renderList << outcome.duration - renderList << outcome.bug?.title - renderList << outcome.bug?.url - renderList << outcome.note + if ("testCase" in columnFields) + renderList << outcome.testCase.fullName + if ("parameters" in columnFields) + renderList << outcome.testCase.parameters + if ("tags" in columnFields) + renderList << outcome.tags + if ("result" in columnFields) + renderList << outcome.testResult.name + if ("streak" in columnFields) + renderList << outcome.testOutcomeStats?.streak + if ("successRate" in columnFields) + renderList << outcome.testOutcomeStats?.successRate + if ("analysisState" in columnFields) + renderList << outcome.analysisState?.name + if ("startedAt" in columnFields) + renderList << outcome.startedAt + if ("finishedAt" in columnFields) + renderList << outcome.finishedAt + if ("duration" in columnFields) + renderList << outcome.duration + if ("bug" in columnFields) + renderList << outcome.bug?.title + if ("owner" in columnFields) + renderList << outcome.owner + if ("note" in columnFields) + renderList << outcome.note + if ("testOutput" in columnFields) + renderList << outcome.testOutput + if ("links" in columnFields) + renderList << (outcome.links.isEmpty() ? null : outcome.links) + + propertyFields.each { propertyField -> + def found = false + outcome.testProperties.each { property -> + if (property.name == propertyField) { + renderList << property.value + found = true + } + } + if (!found) { + renderList << null + } + } + renderList.eachWithIndex { it, indx -> if (it == null) { buff << '' diff --git a/grails/grails-app/views/testRun/_header.gsp b/grails/grails-app/views/testRun/_header.gsp index 90b9884..25f4225 100644 --- a/grails/grails-app/views/testRun/_header.gsp +++ b/grails/grails-app/views/testRun/_header.gsp @@ -26,8 +26,9 @@ along with this program. If not, see . Test Run ${testRun?.dateExecuted?.encodeAsHTML()} - Permalink ${bullet} - Export ${bullet} + + Permalink ${bullet} + Export ${bullet} Edit ${bullet} Delete ${bullet} diff --git a/grails/grails-app/views/testRun/export.gsp b/grails/grails-app/views/testRun/export.gsp index 99c0e9b..32b0838 100644 --- a/grails/grails-app/views/testRun/export.gsp +++ b/grails/grails-app/views/testRun/export.gsp @@ -29,10 +29,10 @@ along with this program. If not, see .
Export Test Run ${testRun?.dateExecuted?.encodeAsHTML()} of Project ${testRun?.project?.name?.encodeAsHTML()}
    -
  • Comma Separated (CSV)
  • -
  • Tab Separated (TSV)
  • -
  • XML
  • -
  • Bucketed TestNG Suite
  • +
  • Comma Separated (CSV)
  • +
  • Tab Separated (TSV)
  • +
  • XML
  • +
  • Bucketed TestNG Suite


diff --git a/grails/web-app/js/cuanto/analysisTable.js b/grails/web-app/js/cuanto/analysisTable.js index 61a685a..9f1bbca 100644 --- a/grails/web-app/js/cuanto/analysisTable.js +++ b/grails/web-app/js/cuanto/analysisTable.js @@ -146,6 +146,7 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN YAHOO.util.Event.addListener("deleteTestRun", "click", deleteTestRun); YAHOO.util.Event.addListener("recalcStats", "click", recalcStats); YAHOO.util.Event.addListener("chooseColumns", "click", chooseColumns); + YAHOO.util.Event.addListener("export", "click", exportResults); new YAHOO.widget.Tooltip("feedtt", {context:"feedImg"}); @@ -319,6 +320,22 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN return newRequest; } + function generateExportRequest() { + var order; + if (dataTable.get("sortedBy").dir == YAHOO.widget.DataTable.CLASS_DESC) { + order = "desc"; + } else { + order = "asc"; + } + var newRequest = "filter=" + getCurrentFilter() + + "&order=" + order + + "&sort=" + dataTable.get("sortedBy").key + + getSearchQuery() + + getTagsQuery() + + "&rand=" + new Date().getTime(); + + return newRequest; + } function onFilterChangeEvent(e, arg) { var filter = arg[0]; @@ -1057,6 +1074,23 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN }); } + // modify the export url as it's clicked to add in current view filter + // query parameters + function exportResults(e) { + var requestString = generateExportRequest(); + + var columns = []; + $.each(dataTable.getColumnSet().flat, function(idx, column) { + var isYui = column.key.match(/^yui/); + if (!column.key.match(/^yui/) && !column.hidden) { + columns.push(column.label + "|" + column.key); + } + }); + + requestString += "&columns=" + columns; + e.target.href += "?" + requestString; + } + function unescapeHtmlEntities(s) { return s.replace(/</g, '<').replace(/>/g, '>'); } From 1de9507ff616f8335a08630d5b135041747d3ab2 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 11:25:56 -0700 Subject: [PATCH 08/21] Better escaping of csv output --- .../controllers/cuanto/TestRunController.groovy | 1 - .../services/cuanto/TestOutcomeService.groovy | 15 +++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/grails/grails-app/controllers/cuanto/TestRunController.groovy b/grails/grails-app/controllers/cuanto/TestRunController.groovy index ba475b5..65bc410 100644 --- a/grails/grails-app/controllers/cuanto/TestRunController.groovy +++ b/grails/grails-app/controllers/cuanto/TestRunController.groovy @@ -520,7 +520,6 @@ class TestRunController { def export = { - println("TestRunController.export( " + params + " )" ) [testRun: TestRun.get(params.id)] } diff --git a/grails/grails-app/services/cuanto/TestOutcomeService.groovy b/grails/grails-app/services/cuanto/TestOutcomeService.groovy index a2e168b..4356505 100644 --- a/grails/grails-app/services/cuanto/TestOutcomeService.groovy +++ b/grails/grails-app/services/cuanto/TestOutcomeService.groovy @@ -485,7 +485,7 @@ class TestOutcomeService { if ("parameters" in columnFields) renderList << outcome.testCase.parameters if ("tags" in columnFields) - renderList << outcome.tags + renderList << (outcome.tags.isEmpty() ? null : outcome.tags) if ("result" in columnFields) renderList << outcome.testResult.name if ("streak" in columnFields) @@ -526,11 +526,18 @@ class TestOutcomeService { renderList.eachWithIndex { it, indx -> if (it == null) { - buff << '' + it = '' } else { - buff << it.toString().replaceAll(delimiter, "\\${delimiter}") + it = it.toString() + it = it.replaceAll(delimiter, "\\${delimiter}") + it = it.replaceAll("\"", "\"\"") + if (it.contains("\"") || it.contains(delimiter) || it.contains("\n")) { + it = "\"${it}\"" + } } - //buff << it != null ? it : '' + + buff << it + if (indx != renderList.size() - 1) { buff << delimiter } From 30791b1bde71de8cd82bad23c07a7346e6242a19 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 11:39:39 -0700 Subject: [PATCH 09/21] Fixed failing test --- grails/test/integration/cuanto/TestOutcomeServiceTests.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails/test/integration/cuanto/TestOutcomeServiceTests.groovy b/grails/test/integration/cuanto/TestOutcomeServiceTests.groovy index f854c60..4dc96f2 100644 --- a/grails/test/integration/cuanto/TestOutcomeServiceTests.groovy +++ b/grails/test/integration/cuanto/TestOutcomeServiceTests.groovy @@ -161,7 +161,7 @@ public class TestOutcomeServiceTests extends GroovyTestCase { outcomes << outcome } - def csv = testOutcomeService.getDelimitedTextForTestOutcomes(outcomes, ",") + def csv = testOutcomeService.getDelimitedTextForTestOutcomes(outcomes, null, ",") def csvLines = csv.readLines() assertEquals "Wrong number of lines for CSV output", outcomes.size() + 1, csvLines.size() From d36517eb6f607672d68ad346bc4217d5e37c65d6 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 19 Aug 2014 14:47:38 -0700 Subject: [PATCH 10/21] Added filter for non-passing results --- .../grails-app/conf/spring/resources.groovy | 2 + .../cuanto/TestRunController.groovy | 1 + .../cuanto/TestOutcomeQueryFilter.groovy | 8 +++- .../services/cuanto/TestOutcomeService.groovy | 2 + .../TestResultIsNonPassingQueryModule.groovy | 46 +++++++++++++++++++ 5 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 grails/grails-app/utils/cuanto/queryprocessor/TestResultIsNonPassingQueryModule.groovy diff --git a/grails/grails-app/conf/spring/resources.groovy b/grails/grails-app/conf/spring/resources.groovy index b91c9be..8f2fb7a 100644 --- a/grails/grails-app/conf/spring/resources.groovy +++ b/grails/grails-app/conf/spring/resources.groovy @@ -29,6 +29,7 @@ import cuanto.parsers.CuantoManualParser import cuanto.parsers.TestNgParser import cuanto.queryprocessor.TestRunQueryModule import cuanto.queryprocessor.TestResultIsFailureQueryModule +import cuanto.queryprocessor.TestResultIsNonPassingQueryModule import cuanto.queryprocessor.TestResultQueryModule import cuanto.queryprocessor.TestCaseFullNameQueryModule import cuanto.queryprocessor.TestCaseParametersQueryModule @@ -75,6 +76,7 @@ beans = { new TestRunQueryModule(), new TestResultIsFailureQueryModule(), new TestResultIsSkipQueryModule(), + new TestResultIsNonPassingQueryModule(), new TestResultQueryModule(), new TestCaseFullNameQueryModule(), new TestCaseParametersQueryModule(), diff --git a/grails/grails-app/controllers/cuanto/TestRunController.groovy b/grails/grails-app/controllers/cuanto/TestRunController.groovy index 65bc410..b2fcee9 100644 --- a/grails/grails-app/controllers/cuanto/TestRunController.groovy +++ b/grails/grails-app/controllers/cuanto/TestRunController.groovy @@ -456,6 +456,7 @@ class TestRunController { filterList += [id: "newpasses", value: "New Passes"] filterList += [id: "allresults", value: "All Results"] filterList += [id: "allquarantined", value: "All Quarantined"] + filterList += [id: "nonPassing", value: "All Non-Passing"] return filterList } diff --git a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy index cb6a39d..c721cdc 100644 --- a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy +++ b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy @@ -29,6 +29,7 @@ public class TestOutcomeQueryFilter implements QueryFilter { testRun(nullable: true) isFailure(nullable: true) isSkip(nullable: true) + isNonPassing(nullable: true) testResult(nullable:true) testCaseFullName(nullable: true) testCaseParameters(nullable: true) @@ -53,7 +54,7 @@ public class TestOutcomeQueryFilter implements QueryFilter { TestOutcomeQueryFilter() {} TestOutcomeQueryFilter(TestOutcomeQueryFilter filterToCopy) { - ["testRun", "isFailure", "isSkip", "testResult", "testCaseFullName", "testCaseParameters", "testCasePackage", "project", + ["testRun", "isFailure", "isSkip", "isNonPassing", "testResult", "testCaseFullName", "testCaseParameters", "testCasePackage", "project", "testResultIncludedInCalculations", "isAnalyzed", "analysisState", "bug", "owner", "testCase", "note", "testOutput", "dateCriteria", "sorts", "queryOffset", "queryMax", "isFailureStatusChanged", "hasAllTestOutcomeProperties", "successRate"].each { @@ -84,6 +85,11 @@ public class TestOutcomeQueryFilter implements QueryFilter { */ Boolean isSkip + /** + * If true then all returned outcomes that are considered failures or skips will be returned. + * If null, outcomes will not be returned based on isNonPassing status + */ + Boolean isNonPassing /** * If not null, then all returned outcomes must have this exact TestResult. diff --git a/grails/grails-app/services/cuanto/TestOutcomeService.groovy b/grails/grails-app/services/cuanto/TestOutcomeService.groovy index 4356505..8df65d3 100644 --- a/grails/grails-app/services/cuanto/TestOutcomeService.groovy +++ b/grails/grails-app/services/cuanto/TestOutcomeService.groovy @@ -405,6 +405,8 @@ class TestOutcomeService { filter.isSkip = true } else if (params.filter?.equalsIgnoreCase("allquarantined")) { filter.analysisState = dataService.getAnalysisStateByName('Quarantined') + } else if (params.filter?.equalsIgnoreCase("nonPassing")) { + filter.isNonPassing = true } if (params.tag) { diff --git a/grails/grails-app/utils/cuanto/queryprocessor/TestResultIsNonPassingQueryModule.groovy b/grails/grails-app/utils/cuanto/queryprocessor/TestResultIsNonPassingQueryModule.groovy new file mode 100644 index 0000000..b7aa640 --- /dev/null +++ b/grails/grails-app/utils/cuanto/queryprocessor/TestResultIsNonPassingQueryModule.groovy @@ -0,0 +1,46 @@ +/* + +Copyright (c) 2010 Todd Wells + +This file is part of Cuanto, a test results repository and analysis program. + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with this program. If not, see . + +*/ + +package cuanto.queryprocessor + +import cuanto.QueryFilter +import cuanto.TestOutcome +public class TestResultIsNonPassingQueryModule implements QueryModule { + + /** + * Based on the value of queryFilter.isNonPassing: + * If true then all returned outcomes that are considered failures or skips will be returned. + * If null, outcomes will not be returned based on failure status. + */ + public Map getQueryParts(QueryFilter queryFilter) { + if (queryFilter.isNonPassing != null) { + return [where: " (t.testResult.isFailure = ? OR t.testResult.isSkip = ?) ", params: [queryFilter.isNonPassing, queryFilter.isNonPassing]] + } else { + return [:] + } + } + + + public List getObjectTypes() { + return [TestOutcome.class] + } + +} \ No newline at end of file From 3503ddd034fee389b59ec311f10eeaf229e751a8 Mon Sep 17 00:00:00 2001 From: toddq Date: Fri, 19 Sep 2014 09:29:10 -0700 Subject: [PATCH 11/21] Don't insert breaks between tab buttons. Let them flow. --- grails/grails-app/views/testRun/_tags.gsp | 3 --- 1 file changed, 3 deletions(-) diff --git a/grails/grails-app/views/testRun/_tags.gsp b/grails/grails-app/views/testRun/_tags.gsp index b9d7750..9fc0f6f 100644 --- a/grails/grails-app/views/testRun/_tags.gsp +++ b/grails/grails-app/views/testRun/_tags.gsp @@ -43,9 +43,6 @@ along with this program. If not, see . - -
-

From 9bffec7deb96c08eee07eea201752768e7cf182b Mon Sep 17 00:00:00 2001 From: toddq Date: Mon, 22 Sep 2014 13:04:07 -0700 Subject: [PATCH 12/21] Changed behavior of tags to AND them together. If multiple tags are selected by the user, the set of results will be those that include ALL the selected tags, not just any of them. --- grails/grails-app/conf/DataSource.groovy | 2 + .../cuanto/TestOutcomeQueryFilter.groovy | 3 ++ .../utils/cuanto/CustomDialect.groovy | 14 ++++++ .../utils/cuanto/QueryBuilder.groovy | 38 ++++++++++++++-- .../queryprocessor/TagNameQueryModule.groovy | 43 +++++++++++++------ 5 files changed, 83 insertions(+), 17 deletions(-) create mode 100644 grails/grails-app/utils/cuanto/CustomDialect.groovy diff --git a/grails/grails-app/conf/DataSource.groovy b/grails/grails-app/conf/DataSource.groovy index 5218538..cd3b4a8 100644 --- a/grails/grails-app/conf/DataSource.groovy +++ b/grails/grails-app/conf/DataSource.groovy @@ -7,6 +7,8 @@ hibernate { cache.use_second_level_cache=true cache.use_query_cache=true cache.provider_class='org.hibernate.cache.EhCacheProvider' + dialect='cuanto.CustomDialect' + //show_sql=true } // environment specific settings environments { diff --git a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy index cb6a39d..6d389d6 100644 --- a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy +++ b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy @@ -226,6 +226,9 @@ public class TestOutcomeQueryFilter implements QueryFilter { def newList = sorts.collect{"t." + it.sort} select += ", " + newList.join(", ") } + if (tags) { + select += ", group_concat(tag_0.name)" + } return select } diff --git a/grails/grails-app/utils/cuanto/CustomDialect.groovy b/grails/grails-app/utils/cuanto/CustomDialect.groovy new file mode 100644 index 0000000..a8738fc --- /dev/null +++ b/grails/grails-app/utils/cuanto/CustomDialect.groovy @@ -0,0 +1,14 @@ +package cuanto + +import org.hibernate.dialect.MySQL5InnoDBDialect; +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.type.StringType; + +class CustomDialect extends MySQL5InnoDBDialect { + + public CustomDialect() { + super() + registerFunction("group_concat", new StandardSQLFunction("group_concat", new StringType())); + } + +} \ No newline at end of file diff --git a/grails/grails-app/utils/cuanto/QueryBuilder.groovy b/grails/grails-app/utils/cuanto/QueryBuilder.groovy index abab4f4..f0b5845 100644 --- a/grails/grails-app/utils/cuanto/QueryBuilder.groovy +++ b/grails/grails-app/utils/cuanto/QueryBuilder.groovy @@ -112,6 +112,8 @@ public class QueryBuilder { def buildQueryForBaseQuery(QueryFilter queryFilter) { List fromClauses = [] List whereClauses = [] + List groupByClauses = [] + List havingClauses = [] List params = [] List processors = getProcessors(queryFilter.appliesToClass()) @@ -125,10 +127,19 @@ public class QueryBuilder { if (details.where?.trim()) { whereClauses << " ${details.where} " - if (details.params) { - params += details.params - } } + + if (details.having?.trim()) { + havingClauses << " ${details.having} " + } + + if (details.group?.trim()) { + groupByClauses << " ${details.group} " + } + + if (details.params) { + params += details.params + } } String selectClause = queryFilter.selectClause() @@ -152,7 +163,26 @@ public class QueryBuilder { } } - String query = selectClause + " " + fromClause + " where " + whereClause + String groupByClause = "" + if (groupByClauses) { + groupByClause = " group by " + groupByClauses.join(", ") + } + + String havingClause = "" + if (havingClauses) { + havingClause = " having " + havingClauses.eachWithIndex {clause, idx -> + havingClause += " ${clause}" + if (idx < havingClauses.size() - 1) { + havingClause += " and " + } + else { + havingClause += " " + } + } + } + + String query = selectClause + " " + fromClause + " where " + whereClause + groupByClause + havingClause return [hql: query.toString(), 'params': params.flatten()] } diff --git a/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy b/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy index ec95df3..8adb6ef 100644 --- a/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy +++ b/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy @@ -28,19 +28,36 @@ import cuanto.TestOutcome public class TagNameQueryModule implements QueryModule { public Map getQueryParts(QueryFilter queryFilter) { - if (queryFilter.tags) { - def whereClauses = [] - def params = [] - queryFilter.tags.each { tagName-> - whereClauses << "upper(tag_0.name) like ?" - params << tagName.toUpperCase() - } - def whereText = "(" + whereClauses.join(" or ") + ")" - return [from: "inner join t.tags tag_0", where: whereText, - 'params': params ] - } else { - return [:] - } + def combineWith = "and" + def parts = [:] + if (queryFilter.tags) { + if (combineWith.equals("or")) { + def whereClauses = [] + def params = [] + queryFilter.tags.each { tagName-> + whereClauses << "upper(tag_0.name) like ?" + params << tagName.toUpperCase() + } + def whereText = "(" + whereClauses.join(" OR ") + ")" + parts = [from: "inner join t.tags tag_0", where: whereText, + 'params': params ] + } else { + def having = [] + def params = [] + queryFilter.tags.each { tagName-> + //having << "find_in_set(?, upper(col_3_0_))" + //params << tagName.toUpperCase() + having << "upper(col_3_0_) like ?" + params << "%" + tagName.toUpperCase() + "%" + } + parts = [from: "inner join t.tags tag_0", + group: "t.id", + having: "(" + having.join(" AND ") + ")", + params: params] + } + + } + return parts } From 731ec60932df47a95e8e12df5ea8a51fb4c80448 Mon Sep 17 00:00:00 2001 From: toddq Date: Tue, 23 Sep 2014 09:28:32 -0700 Subject: [PATCH 13/21] Allow setting purge days on project creation --- grails/grails-app/services/cuanto/ProjectService.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grails/grails-app/services/cuanto/ProjectService.groovy b/grails/grails-app/services/cuanto/ProjectService.groovy index e4bc509..d8ba9a7 100644 --- a/grails/grails-app/services/cuanto/ProjectService.groovy +++ b/grails/grails-app/services/cuanto/ProjectService.groovy @@ -90,7 +90,9 @@ class ProjectService { parsedProject.name = params?.name parsedProject.projectKey = params?.projectKey parsedProject.testType = params?.testType - parsedProject.purgeDays = params?.purgeDays + if (params?.purgeDays) { + parsedProject.purgeDays = params.purgeDays.toInteger() + } return createProject(parsedProject) } From fe28bf47e96ca8fc85b9389bb6221c6cb296d317 Mon Sep 17 00:00:00 2001 From: toddq Date: Wed, 24 Sep 2014 10:33:39 -0700 Subject: [PATCH 14/21] Prevent NPE when outcome does not have a duration. --- grails/grails-app/services/cuanto/StatisticService.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails/grails-app/services/cuanto/StatisticService.groovy b/grails/grails-app/services/cuanto/StatisticService.groovy index 597facc..83f66b4 100644 --- a/grails/grails-app/services/cuanto/StatisticService.groovy +++ b/grails/grails-app/services/cuanto/StatisticService.groovy @@ -347,7 +347,7 @@ class StatisticService { tagStat.skipped = skipped ?: 0; log.debug "${skipped} skipped for ${tag.name}" - def duration = rawStats.collect { it[iDuration] }.sum() + def duration = rawStats.collect { it[iDuration] ?: 0 }.sum() tagStat.duration = Math.max(0, duration ?: 0); def total = rawStats.collect {it[iCount]}.sum() From bad43a71fad6c06479aaa0b3e4acbba634faa34a Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 25 Sep 2014 10:30:53 -0700 Subject: [PATCH 15/21] Stats Service fixes - removed unused processingTestRunStats - fixed race condition by dequeuing stat request before processing it - re-queue requests to end of queue if exception so other requests aren't starved - outcome stats only need to be calculated once, not every single time - increased sleep time between calculations --- grails/grails-app/conf/Config.groovy | 2 +- grails/grails-app/jobs/TestRunStatsJob.groovy | 4 +- .../services/cuanto/StatisticService.groovy | 69 ++++++++++--------- 3 files changed, 39 insertions(+), 36 deletions(-) diff --git a/grails/grails-app/conf/Config.groovy b/grails/grails-app/conf/Config.groovy index e7a04e8..e4bcab1 100644 --- a/grails/grails-app/conf/Config.groovy +++ b/grails/grails-app/conf/Config.groovy @@ -92,7 +92,7 @@ uiperformance.exclusions = [ ] // statSleep is the time to sleep between calculating test run stats -statSleep = 1000 +statSleep = 3000 // testOutcomeAndTestRunInitSleep is the time to sleep between initializing TestOutcomes and TestRuns testOutcomeAndTestRunInitSleepTime = 5000 diff --git a/grails/grails-app/jobs/TestRunStatsJob.groovy b/grails/grails-app/jobs/TestRunStatsJob.groovy index b9d2b76..9a5f5b4 100644 --- a/grails/grails-app/jobs/TestRunStatsJob.groovy +++ b/grails/grails-app/jobs/TestRunStatsJob.groovy @@ -26,8 +26,6 @@ class TestRunStatsJob { } def execute() { - if (!statisticService.processingTestRunStats) { - statisticService.processTestRunStats() - } + statisticService.processTestRunStats() } } diff --git a/grails/grails-app/services/cuanto/StatisticService.groovy b/grails/grails-app/services/cuanto/StatisticService.groovy index 83f66b4..c6d8163 100644 --- a/grails/grails-app/services/cuanto/StatisticService.groovy +++ b/grails/grails-app/services/cuanto/StatisticService.groovy @@ -35,7 +35,6 @@ class StatisticService { def grailsApplication int numRecentTestOutcomes = 40 - Boolean processingTestRunStats = false; final private static String queueLock = "Test Run Stat Queue Lock" final private static String calcLock = "Test Run Calculation Lock" @@ -107,28 +106,27 @@ class StatisticService { log.debug "${queueSize} items in stat queue" QueuedTestRunStat queuedItem = getFirstTestRunIdInQueue() if (queuedItem) { + log.info "Calculating stats for test run ${queuedItem.testRunId}" + def startTime = System.currentTimeMillis() try { QueuedTestRunStat.withTransaction { calculateTestRunStats(queuedItem.testRunId) calculateTestOutcomeStats(queuedItem.testRunId) - queuedItem.delete(flush: true) queueSize = QueuedTestRunStat.list().size() } - } catch (OptimisticLockingFailureException e) { - log.info "OptimisticLockingFailureException for test run ${queuedItem.testRunId}" - // leave it in queue so it gets tried again - } catch (HibernateOptimisticLockingFailureException e) { - log.info "HibernateOptimisticLockingFailureException for test run ${queuedItem.testRunId}" - } catch (StaleObjectStateException e) { - log.info "StaleObjectStateException for test run ${queuedItem.testRunId}" - // leave it in queue so it gets tried again - } + def elapsed = System.currentTimeMillis() - startTime + log.info "Calculated stats for ${queuedItem.testRunId} in ${elapsed} ms" + } catch (Exception e) { + // re-queue to the "end" of the queue so other items aren't starved + log.info "Exception for test run ${queuedItem.testRunId} - ${e.toString()}" + queuedItem.dateCreated = new Date() + dataService.saveDomainObject(queuedItem, true) + } } + if (grailsApplication.config.statSleep) { + sleep(grailsApplication.config.statSleep) + } } - if (grailsApplication.config.statSleep) { - sleep(grailsApplication.config.statSleep) - } - } } @@ -143,12 +141,17 @@ class StatisticService { QueuedTestRunStat getFirstTestRunIdInQueue() { - def latest = QueuedTestRunStat.listOrderByDateCreated(max: 1) - if (latest.size() == 0) { - return null - } else { - return latest[0] - } + synchronized (queueLock) { + QueuedTestRunStat.withTransaction { + def queuedItem = null + def latest = QueuedTestRunStat.listOrderByDateCreated(max: 1) + if (latest.size() != 0) { + queuedItem = latest[0] + queuedItem.delete(flush: true) + } + return queuedItem + } + } } @@ -250,16 +253,18 @@ class StatisticService { List testOutcomes = TestOutcome.findAllByTestRun(testRun) for (TestOutcome testOutcome : testOutcomes) { - testOutcome.lock() - def stats = new TestOutcomeStats() - dataService.saveDomainObject(stats, true) - testOutcome.testOutcomeStats = stats - List recentTestResults = TestOutcome.executeQuery( - "SELECT to.testResult FROM cuanto.TestOutcome to WHERE to.testCase = ?", - [testOutcome.testCase], [max: numRecentTestOutcomes, sort: 'id', order: 'desc']) - calculateStreak(testOutcome, recentTestResults) - calculateSuccessRate(testOutcome, recentTestResults) - testOutcome.save() + if (!testOutcome.testOutcomeStats) { + testOutcome.lock() + def stats = new TestOutcomeStats() + dataService.saveDomainObject(stats, true) + testOutcome.testOutcomeStats = stats + List recentTestResults = TestOutcome.executeQuery( + "SELECT to.testResult FROM cuanto.TestOutcome to WHERE to.testCase = ?", + [testOutcome.testCase], [max: numRecentTestOutcomes, sort: 'id', order: 'desc']) + calculateStreak(testOutcome, recentTestResults) + calculateSuccessRate(testOutcome, recentTestResults) + testOutcome.save() + } } } } @@ -324,7 +329,7 @@ class StatisticService { List getTagStatistics(TestRun testRun) { - testRun = testRun.refresh() + testRun = testRun.refresh() def tagStats = [] if (Environment.current != Environment.TEST) { // The HSQL database doesn't like the following query, so we won't do it in testing. From ae70205ea72e54941408568a297281d60c0355df Mon Sep 17 00:00:00 2001 From: toddq Date: Fri, 26 Sep 2014 12:09:52 -0700 Subject: [PATCH 16/21] Fixed filtering by both tag and properties Also fixed tests to pass --- grails/grails-app/conf/DataSource.groovy | 14 ++++ .../grails-app/conf/spring/resources.groovy | 4 +- .../cuanto/TestOutcomeQueryFilter.groovy | 19 +++-- .../queryprocessor/TagNameQueryModule.groovy | 4 +- .../cuanto/TestOutcomeQueryFilterTests.groovy | 84 ++++++++++++------- grails/test/unit/QueryBuilderTests.groovy | 4 +- 6 files changed, 82 insertions(+), 47 deletions(-) diff --git a/grails/grails-app/conf/DataSource.groovy b/grails/grails-app/conf/DataSource.groovy index cd3b4a8..25c4129 100644 --- a/grails/grails-app/conf/DataSource.groovy +++ b/grails/grails-app/conf/DataSource.groovy @@ -23,6 +23,9 @@ environments { } } test { + hibernate { + dialect='org.hibernate.dialect.HSQLDialect' + } dataSource { dbCreate = "update" driverClassName = "org.hsqldb.jdbcDriver" @@ -30,6 +33,17 @@ environments { username = "sa" password = "" } + // use this for tag filter tests which are dependent on MySQL + /* + dataSource { + dbCreate = "update" + pooled = true + username = "root" + password = "" + driverClassName = "com.mysql.jdbc.Driver" + url = "jdbc:mysql://localhost:3306/cuanto-test?autoreconnect=true" + } + */ } production { dataSource { diff --git a/grails/grails-app/conf/spring/resources.groovy b/grails/grails-app/conf/spring/resources.groovy index 8f2fb7a..2f42b2e 100644 --- a/grails/grails-app/conf/spring/resources.groovy +++ b/grails/grails-app/conf/spring/resources.groovy @@ -90,11 +90,11 @@ beans = { new TestCaseQueryModule(), new NoteQueryModule(), new TestOutputQueryModule(), - new TagNameQueryModule(), new HasTagsQueryModule(), new DateExecutedQueryModule(), new FailureStatusChangedQueryModule(), - new TestOutcomeHasAllPropertiesQueryModule() + new TestOutcomeHasAllPropertiesQueryModule(), + new TagNameQueryModule() ] } } \ No newline at end of file diff --git a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy index 45c727f..1cd748b 100644 --- a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy +++ b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy @@ -227,14 +227,15 @@ public class TestOutcomeQueryFilter implements QueryFilter { String selectClause() { - def select = "select distinct t" + def select = "select distinct " + if (tags) { + select += "group_concat(tag_0.name), " + } + select += " t" if (sorts) { def newList = sorts.collect{"t." + it.sort} select += ", " + newList.join(", ") } - if (tags) { - select += ", group_concat(tag_0.name)" - } return select } @@ -309,10 +310,10 @@ public class TestOutcomeQueryFilter implements QueryFilter { public List resultTransform(List results) { - if (sorts) { - return results.collect{it[0]} - } else { - return results - } + if (sorts || tags) { + return results.collect{ it[tags ? 1 : 0] } + } else { + return results + } } } \ No newline at end of file diff --git a/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy b/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy index 8adb6ef..413a3d5 100644 --- a/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy +++ b/grails/grails-app/utils/cuanto/queryprocessor/TagNameQueryModule.groovy @@ -45,9 +45,7 @@ public class TagNameQueryModule implements QueryModule { def having = [] def params = [] queryFilter.tags.each { tagName-> - //having << "find_in_set(?, upper(col_3_0_))" - //params << tagName.toUpperCase() - having << "upper(col_3_0_) like ?" + having << "upper(col_0_0_) like ?" params << "%" + tagName.toUpperCase() + "%" } parts = [from: "inner join t.tags tag_0", diff --git a/grails/test/integration/cuanto/TestOutcomeQueryFilterTests.groovy b/grails/test/integration/cuanto/TestOutcomeQueryFilterTests.groovy index dc19cad..d2de431 100644 --- a/grails/test/integration/cuanto/TestOutcomeQueryFilterTests.groovy +++ b/grails/test/integration/cuanto/TestOutcomeQueryFilterTests.groovy @@ -1,6 +1,7 @@ package cuanto import cuanto.test.TestObjects +import org.codehaus.groovy.grails.commons.ApplicationHolder /** * User: Todd Wells @@ -84,6 +85,10 @@ public class TestOutcomeQueryFilterTests extends GroovyTestCase { void testFilterByTagsAndTestRun() { + // tests involving tags depend on MySQL specific code + if (!ApplicationHolder.application.config.dataSource.driverClassName.contains("mysql")) { + return + } def testCases = [] def numOutcomes = 10 0.upto(numOutcomes - 1) { @@ -122,48 +127,65 @@ public class TestOutcomeQueryFilterTests extends GroovyTestCase { outcomes[6].addToTags(tags[0]) outcomes[6].addToTags(tags[1]) outcomes[6].addToTags(tags[2]) - outcomes[6].addToTags(tags[3]) + + outcomes[7].addToTags(tags[0]) outcomes.each { dataService.saveDomainObject it, true } + // 1 tag - tag 0, 4 tests TestOutcomeQueryFilter queryFilterA = new TestOutcomeQueryFilter() queryFilterA.testRun = testRun - queryFilterA.tags = tags.collect{it.name} + queryFilterA.tags = [tags[0].name] def fetchedOutcomes = dataService.getTestOutcomes(queryFilterA) - assertEquals "Wrong number of TestOutcomes returned", 7, fetchedOutcomes.size() + assertEquals "Wrong number of TestOutcomes returned", 4, fetchedOutcomes.size() + // try it also with sorts + queryFilterA.sorts = [] + queryFilterA.sorts << new SortParameters(sort: "testRun.dateExecuted", sortOrder: "desc") + queryFilterA.sorts << new SortParameters(sort: "testRun.lastUpdated", sortOrder: "desc") + fetchedOutcomes = dataService.getTestOutcomes(queryFilterA) + assertEquals "Wrong number of TestOutcomes returned", 4, fetchedOutcomes.size() + + // 2 tags - tag 0, 1 - 2 tests (4, 6) TestOutcomeQueryFilter queryFilterB = new TestOutcomeQueryFilter() queryFilterB.testRun = testRun queryFilterB.tags = tags[0..1].collect{ it.name } fetchedOutcomes = dataService.getTestOutcomes(queryFilterB) - assertEquals "Wrong number of TestOutcomes returned", 4, fetchedOutcomes.size() + assertEquals "Wrong number of TestOutcomes returned", 2, fetchedOutcomes.size() fetchedOutcomes.each { outcome -> - assertNotNull "Couldnt find a matching tag", outcome.tags.find {it.name == tags[0].name} || outcome.tags.find {it.name == tags[1].name} + assertNotNull "Couldnt find a matching tag", outcome.tags.find {it.name == tags[0].name} && outcome.tags.find {it.name == tags[1].name} } // now do all uppercase, tags should still be found queryFilterB.tags = tags[0..1].collect{ it.name.toUpperCase() } fetchedOutcomes = dataService.getTestOutcomes(queryFilterB) - assertEquals "Wrong number of TestOutcomes returned", 4, fetchedOutcomes.size() + assertEquals "Wrong number of TestOutcomes returned", 2, fetchedOutcomes.size() fetchedOutcomes.each { outcome -> - assertNotNull "Couldnt find a matching tag", outcome.tags.find {it.name == tags[0].name} || outcome.tags.find {it.name == tags[1].name} + assertNotNull "Couldnt find a matching tag", outcome.tags.find {it.name == tags[0].name} && outcome.tags.find {it.name == tags[1].name} } - // now search for TestOutcomes without any tags + // all tags - 0 tests TestOutcomeQueryFilter queryFilterC = new TestOutcomeQueryFilter() queryFilterC.testRun = testRun + queryFilterC.sorts = queryFilterA.sorts + queryFilterC.tags = tags.collect{ it.name } + fetchedOutcomes = dataService.getTestOutcomes(queryFilterC) + assertEquals "Wrong number of TestOutcomes returned", 0, fetchedOutcomes.size() + + // now search for TestOutcomes without any tags + queryFilterC = new TestOutcomeQueryFilter() queryFilterC.hasTags = false fetchedOutcomes = dataService.getTestOutcomes(queryFilterC) - assertEquals "Wrong number of TestOutcomes returned", 3, fetchedOutcomes.size() + assertEquals "Wrong number of TestOutcomes returned", 2, fetchedOutcomes.size() queryFilterC.hasTags = true fetchedOutcomes = dataService.getTestOutcomes(queryFilterC) - assertEquals "Wrong number of TestOutcomes returned", 7, fetchedOutcomes.size() + assertEquals "Wrong number of TestOutcomes returned", 8, fetchedOutcomes.size() } @@ -239,32 +261,32 @@ public class TestOutcomeQueryFilterTests extends GroovyTestCase { fetchedOutcomes = dataService.getTestOutcomes(queryfilterD) assertEquals "Wrong number of TestOutcomes returned", 0, fetchedOutcomes.size() - println "****** ${Tag.count()} tags" + // tests involving tags depend on MySQL specific code + if (ApplicationHolder.application.config.dataSource.driverClassName.contains("mysql")) { + println "****** ${Tag.count()} tags" - TestOutcomeQueryFilter queryFilterWithTagsB = new TestOutcomeQueryFilter() - queryFilterWithTagsB.testRun = testRun - //queryFilterWithTagsB.hasAllTestOutcomeProperties = [new TestOutcomeProperty("john", "lennon")] - queryFilterWithTagsB.tags = ["cool"] + TestOutcomeQueryFilter queryFilterWithTagsB = new TestOutcomeQueryFilter() + queryFilterWithTagsB.testRun = testRun + queryFilterWithTagsB.tags = ["cool"] - fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTagsB) - assertEquals "Wrong number of TestOutcomes returned", 3, fetchedOutcomes.size() + fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTagsB) + assertEquals "Wrong number of TestOutcomes returned", 3, fetchedOutcomes.size() + TestOutcomeQueryFilter queryFilterWithTags = new TestOutcomeQueryFilter() + queryFilterWithTags.testRun = testRun + queryFilterWithTags.hasAllTestOutcomeProperties = [new TestOutcomeProperty("john", "lennon")] + queryFilterWithTags.tags = ["cool"] - TestOutcomeQueryFilter queryFilterWithTags = new TestOutcomeQueryFilter() - queryFilterWithTags.testRun = testRun - queryFilterWithTags.hasAllTestOutcomeProperties = [new TestOutcomeProperty("john", "lennon")] - queryFilterWithTags.tags = ["cool"] + fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTags) + assertEquals "Wrong number of TestOutcomes returned", 3, fetchedOutcomes.size() - fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTags) - assertEquals "Wrong number of TestOutcomes returned", 3, fetchedOutcomes.size() - - TestOutcomeQueryFilter queryFilterWithTagsC = new TestOutcomeQueryFilter() - queryFilterWithTagsC.testRun = testRun - queryFilterWithTagsC.hasAllTestOutcomeProperties = [new TestOutcomeProperty("george", "harrison")] - queryFilterWithTagsC.tags = ["cool"] - - fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTagsC) - assertEquals "Wrong number of TestOutcomes returned", 1, fetchedOutcomes.size() + TestOutcomeQueryFilter queryFilterWithTagsC = new TestOutcomeQueryFilter() + queryFilterWithTagsC.testRun = testRun + queryFilterWithTagsC.hasAllTestOutcomeProperties = [new TestOutcomeProperty("george", "harrison")] + queryFilterWithTagsC.tags = ["cool"] + fetchedOutcomes = dataService.getTestOutcomes(queryFilterWithTagsC) + assertEquals "Wrong number of TestOutcomes returned", 1, fetchedOutcomes.size() + } } } \ No newline at end of file diff --git a/grails/test/unit/QueryBuilderTests.groovy b/grails/test/unit/QueryBuilderTests.groovy index 088cb49..f366dac 100644 --- a/grails/test/unit/QueryBuilderTests.groovy +++ b/grails/test/unit/QueryBuilderTests.groovy @@ -441,8 +441,8 @@ public class QueryBuilderTests extends GroovyTestCase { qf.sorts << new SortParameters(sort: "testRun.dateExecuted", sortOrder: "desc") CuantoQuery expectedQuery = new CuantoQuery() - expectedQuery.hql = "select distinct t, t.testRun.dateExecuted from cuanto.TestOutcome t inner join t.tags tag_0 where t.testRun = ? and t.testRun.dateExecuted > ? and t.testRun.dateExecuted < ? and (upper(tag_0.name) like ? or upper(tag_0.name) like ?) order by t.testRun.dateExecuted desc" - expectedQuery.positionalParameters = [qf.testRun, qf.dateCriteria[0].date, qf.dateCriteria[1].date, qf.tags[0].toUpperCase(), qf.tags[1].toUpperCase()] + expectedQuery.hql = "select distinct group_concat(tag_0.name), t, t.testRun.dateExecuted from cuanto.TestOutcome t inner join t.tags tag_0 where t.testRun = ? and t.testRun.dateExecuted > ? and t.testRun.dateExecuted < ? group by t.id having (upper(col_0_0_) like ? AND upper(col_0_0_) like ?) order by t.testRun.dateExecuted desc" + expectedQuery.positionalParameters = [qf.testRun, qf.dateCriteria[0].date, qf.dateCriteria[1].date, "%${qf.tags[0].toUpperCase()}%", "%${qf.tags[1].toUpperCase()}%"] expectedQuery.paginateParameters = [:] CuantoQuery actualQuery = queryBuilder.buildQuery(qf) From 0bd1ab2f10ec749fc0152a30532b7a2c214982b5 Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 13 Nov 2014 15:38:25 -0800 Subject: [PATCH 17/21] Fix for displayed row counts When fetching test outcomes, separate database calls are made to get the paginated result data and the count of total matching results. The counting query needed to be modified to take into account changes made when including tags in the query filter so that the count number is accurate. --- .../cuanto/TestOutcomeQueryFilter.groovy | 4 +- .../services/cuanto/DataService.groovy | 2 +- .../utils/cuanto/QueryBuilder.groovy | 50 ++++++++++++++++--- 3 files changed, 47 insertions(+), 9 deletions(-) diff --git a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy index 1cd748b..74dab22 100644 --- a/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy +++ b/grails/grails-app/domain/cuanto/TestOutcomeQueryFilter.groovy @@ -245,7 +245,9 @@ public class TestOutcomeQueryFilter implements QueryFilter { String countClause() { - "select count(distinct t) " + // don't know how to combine group_concat with a count + //"select count(distinct t) " + return selectClause() } diff --git a/grails/grails-app/services/cuanto/DataService.groovy b/grails/grails-app/services/cuanto/DataService.groovy index c690728..f1e0413 100644 --- a/grails/grails-app/services/cuanto/DataService.groovy +++ b/grails/grails-app/services/cuanto/DataService.groovy @@ -393,7 +393,7 @@ class DataService { } else { results = TestOutcome.executeQuery(cuantoQuery.hql) } - return results[0] + return results.size() } diff --git a/grails/grails-app/utils/cuanto/QueryBuilder.groovy b/grails/grails-app/utils/cuanto/QueryBuilder.groovy index f0b5845..b46cb55 100644 --- a/grails/grails-app/utils/cuanto/QueryBuilder.groovy +++ b/grails/grails-app/utils/cuanto/QueryBuilder.groovy @@ -33,8 +33,12 @@ public class QueryBuilder { Map result List fromClauses = [] List whereClauses = [] + List groupByClauses = [] + List havingClauses = [] List params = [] + List processors = getProcessors(queryFilter.appliesToClass()) + processors.each {QueryModule queryProcessor -> def details = queryProcessor.getQueryParts(queryFilter) @@ -44,18 +48,32 @@ public class QueryBuilder { if (details.where?.trim()) { whereClauses << " ${details.where} " - if (details.params) { - params += details.params - } + } + + if (details.having?.trim()) { + havingClauses << " ${details.having} " + } + + if (details.group?.trim()) { + groupByClauses << " ${details.group} " + } + + if (details.params) { + params += details.params } } + + String selectClause = queryFilter.countClause() + String fromClause = queryFilter.fromClause() fromClauses.each { fromClause += " ${it} " } + if (whereClauses.size() == 0) { throw new CuantoException("No filter options were specified for query") } + String whereClause = "" whereClauses.eachWithIndex {clause, idx -> whereClause += " ${clause}" @@ -65,15 +83,33 @@ public class QueryBuilder { whereClause += " " } } - String selectClause = queryFilter.countClause() - String query = selectClause + " " + fromClause + " where " + whereClause + + String groupByClause = "" + if (groupByClauses) { + groupByClause = " group by " + groupByClauses.join(", ") + } + + String havingClause = "" + if (havingClauses) { + havingClause = " having " + havingClauses.eachWithIndex {clause, idx -> + havingClause += " ${clause}" + if (idx < havingClauses.size() - 1) { + havingClause += " and " + } + else { + havingClause += " " + } + } + } + + String query = selectClause + " " + fromClause + " where " + whereClause + groupByClause + havingClause result = [hql: query.toString(), 'params': params.flatten()] return new CuantoQuery(hql: result.hql as String, positionalParameters: result.params as List) } CuantoQuery buildQuery(QueryFilter queryFilter) throws CuantoException { - Map base = buildQueryForBaseQuery(queryFilter) String query = base.hql as String @@ -130,7 +166,7 @@ public class QueryBuilder { } if (details.having?.trim()) { - havingClauses << " ${details.having} " + havingClauses << " ${details.having} " } if (details.group?.trim()) { From 99692a0d58df1d80367512bb70a73ca4c8dcd482 Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 13 Nov 2014 15:39:13 -0800 Subject: [PATCH 18/21] Fix packaging hang --- grails/scripts/_BuildCuanto.groovy | 1 + 1 file changed, 1 insertion(+) diff --git a/grails/scripts/_BuildCuanto.groovy b/grails/scripts/_BuildCuanto.groovy index 48bdd24..459844d 100644 --- a/grails/scripts/_BuildCuanto.groovy +++ b/grails/scripts/_BuildCuanto.groovy @@ -59,6 +59,7 @@ Closure getModulePackager(moduleName, moduleDir, pomXml, targetDir) println "beginning packaging" def packageProcess = "mvn -f ${moduleDir}/pom.xml clean install".execute() + packageProcess.in.eachLine { line -> println line } packageProcess.waitFor() println "clean package done" if (packageProcess.exitValue() != 0) { From f7894074f26a58879de111b41c0236c7f751208f Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 13 Nov 2014 15:41:36 -0800 Subject: [PATCH 19/21] Fix base url in rss feed. Item links still not valid. --- grails/grails-app/controllers/cuanto/ProjectController.groovy | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/grails/grails-app/controllers/cuanto/ProjectController.groovy b/grails/grails-app/controllers/cuanto/ProjectController.groovy index dae6b4d..709a724 100644 --- a/grails/grails-app/controllers/cuanto/ProjectController.groovy +++ b/grails/grails-app/controllers/cuanto/ProjectController.groovy @@ -202,9 +202,10 @@ class ProjectController { if (params.id) { Project proj = Project.get(params.id) if (proj) { + def urlBase = request.scheme + "://" + request.serverName + ":" + request.serverPort + "/" + grailsApplication.metadata.'app.name' render(feedType: "rss", feedVersion: "2.0") { title = "${proj.toString()} Recent Results" - link = "http://your.test.server/yourController/feed" + link = createLink(controller: "project", action: "feed", id: proj.id, base: urlBase) description = "Recent Test Results for ${proj.toString()}" List testRuns = testRunService.getTestRunsForProject([id: proj.id, offset: 0, max: Defaults.ItemsPerFeed, @@ -214,6 +215,7 @@ class ProjectController { def stats = TestRunStats.findByTestRun(testRun) if (stats){ entry(testRun.dateExecuted) { + // TODO: summary does not appear to be a valid action. what's it supposed to link to? link = createLink(controller: "testRun", action: "summary", id: testRun.id) def feedTxt = testRunService.getFeedText(testRun, stats) return feedTxt From 907ecefcaff327e6eac039d62efb1fd03a995f17 Mon Sep 17 00:00:00 2001 From: toddq Date: Thu, 13 Nov 2014 15:51:10 -0800 Subject: [PATCH 20/21] Batch process test outcome stats Calculation of the test outcome stats (streaks and success rate) for every test outcome is expensive and was starving out calculation of test run stats. This changes makes the stats service crunch stats in batches, takes breaks to let other things be processed. --- .../services/cuanto/StatisticService.groovy | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/grails/grails-app/services/cuanto/StatisticService.groovy b/grails/grails-app/services/cuanto/StatisticService.groovy index c6d8163..cdf4c65 100644 --- a/grails/grails-app/services/cuanto/StatisticService.groovy +++ b/grails/grails-app/services/cuanto/StatisticService.groovy @@ -34,6 +34,9 @@ class StatisticService { def testRunService def grailsApplication + def outcomeBatchSize = 100 + def queuedTestResultStats = [] as Set + int numRecentTestOutcomes = 40 final private static String queueLock = "Test Run Stat Queue Lock" final private static String calcLock = "Test Run Calculation Lock" @@ -102,17 +105,16 @@ class StatisticService { def processTestRunStats() { synchronized (calcLock) { def queueSize = QueuedTestRunStat.list().size() - while (queueSize > 0) { - log.debug "${queueSize} items in stat queue" + while (queueSize > 0 || queuedTestResultStats.size() > 0) { + log.debug "${queueSize} items in stat queue and ${queuedTestResultStats.size()} in result stat queue" QueuedTestRunStat queuedItem = getFirstTestRunIdInQueue() if (queuedItem) { log.info "Calculating stats for test run ${queuedItem.testRunId}" def startTime = System.currentTimeMillis() try { + queuedTestResultStats.add(queuedItem.testRunId) QueuedTestRunStat.withTransaction { calculateTestRunStats(queuedItem.testRunId) - calculateTestOutcomeStats(queuedItem.testRunId) - queueSize = QueuedTestRunStat.list().size() } def elapsed = System.currentTimeMillis() - startTime log.info "Calculated stats for ${queuedItem.testRunId} in ${elapsed} ms" @@ -122,10 +124,16 @@ class StatisticService { queuedItem.dateCreated = new Date() dataService.saveDomainObject(queuedItem, true) } - } + } else { + // process some queued test outcome stats + QueuedTestRunStat.withTransaction { + calculateTestOutcomeStats(queuedTestResultStats.toList().first()) + } + } if (grailsApplication.config.statSleep) { sleep(grailsApplication.config.statSleep) } + queueSize = QueuedTestRunStat.list().size() } } } @@ -250,9 +258,10 @@ class StatisticService { if (!testRun) { log.error "Couldn't find test run ${testRunId}" } else { + def counter = outcomeBatchSize + def startTime = System.currentTimeMillis() List testOutcomes = TestOutcome.findAllByTestRun(testRun) - for (TestOutcome testOutcome : testOutcomes) - { + for (TestOutcome testOutcome : testOutcomes) { if (!testOutcome.testOutcomeStats) { testOutcome.lock() def stats = new TestOutcomeStats() @@ -264,8 +273,15 @@ class StatisticService { calculateStreak(testOutcome, recentTestResults) calculateSuccessRate(testOutcome, recentTestResults) testOutcome.save() + if (counter-- <= 0) { + log.info "processed a batch of ${outcomeBatchSize} outcome stats for test run ${testRunId} in " + (System.currentTimeMillis() - startTime) + " ms" + return + } } } + // made it to the end of for every testOutcome! + log.info "finished processing test outcome stats for test run ${testRunId} in " + (System.currentTimeMillis() - startTime) + " ms" + queuedTestResultStats.remove(testRunId) } } From 196b9b918dda7204bb6234ab022a9ccc21f973e6 Mon Sep 17 00:00:00 2001 From: toddq Date: Mon, 19 Jan 2015 08:58:02 -0800 Subject: [PATCH 21/21] Allow filters from query params. This commit only adds support for tag filters. If there's tags specified as query params to this page, use them to set the defaults for the view. --- grails/web-app/js/cuanto/analysisTable.js | 29 ++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/grails/web-app/js/cuanto/analysisTable.js b/grails/web-app/js/cuanto/analysisTable.js index 9f1bbca..cf4459b 100644 --- a/grails/web-app/js/cuanto/analysisTable.js +++ b/grails/web-app/js/cuanto/analysisTable.js @@ -439,9 +439,14 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN } else { setTcFormatPref(); } - return "format=json&offset=0&max=" + getRowsPerPage(defaultRowsPerPage) + - "&order=asc&sort=testCase&filter=" + getCurrentFilter() + - "&tcFormat=" + tcFormat + "&rand=" + new Date().getTime(); + return "format=json" + + "&offset=0" + + "&max=" + getRowsPerPage(defaultRowsPerPage) + + "&order=asc" + + "&sort=testCase" + + "&filter=" + getCurrentFilter() + + "&tcFormat=" + tcFormat + + "&rand=" + new Date().getTime(); } @@ -1156,6 +1161,24 @@ YAHOO.cuanto.AnalysisTable = function(testResultNames, analysisStateNames, propN $.each($('.tagspan'), function(idx, tagspan) { tagButtons.push(new YAHOO.widget.Button(tagspan.id, {type: "checkbox", checked: false, onclick: { fn: onTagClick } })); }); + + // if there's a tags query param, toggle those tag buttons on + if (YAHOO.util.History.getQueryStringParameter('tags') ) { + var removeUnchecked = true; + var tags = YAHOO.util.History.getQueryStringParameter('tags').split(","); + // loop over tag buttons + for (var i = 2; i < tagButtons.length; i++) { + var button = tagButtons[i]; + if (tags.indexOf(button.get("label")) >= 0) { + button.set("checked", true); + } else { + if (removeUnchecked) { + $(button._button).parents('.tagspan').remove(); + } + } + } + onFilterChange(null); + } } function onTagClick(e) {