"Fossies" - the Fresh Open Source Software Archive  

Source code changes of the file "projects/MatrixTesting/src/matrix-query.C" between
rose-0.11.53.0.tar.gz and rose-0.11.54.0.tar.gz

About: ROSE is a compiler infrastructure to build source-to-source program transformation and analysis tools for large-scale C, C++, UPC, Fortran, OpenMP, Java, Python and PHP applications.

matrix-query.C  (rose-0.11.53.0):matrix-query.C  (rose-0.11.54.0)
static const char *gPurpose = "query test results"; static const char *gPurpose = "query test results";
static const char *gDescription = static const char *gDescription =
"Queries a database to show the matrix testing results. The arguments are c olumn names (use \"list\" to " "Queries a database to show the matrix testing results. The arguments are c olumn names (use \"list\" to "
"get a list of valid column names). They can be in two forms: a bare column "get a list of valid column names). They can be in two forms: constraints an
name causes the table to " d displays. A constraint argument is "
"contain that column, but if the column name is followed by an equal sign an "a column name, comparison operator, and value; and a display argument is co
d a value, then the table " lumn name and optional "
"is restricted to rows that have that value for the column, and the constant "sorting direction.\n\n"
-valued column is displayed "
"above the table instead (if you also want it in the table, then also specif "The relational operators for constraints are designed to work well with she
y its bare name). If no columns " ll scripts. They are:"
"are specified then all of them are shown (the special \"all\" column does t "@named{=}{The column on the left hand side must be equal to the value on th
he same thing). Since more than " e right hand side.}"
"one test might match the selection criteria, the final column is \"count\" "@named{/}{The column on the left hand side must be not-equal to the value o
to say how many such rows " n the right hand sie.}"
"are present in the database."; "@named{+}{The column on the left hand size must be greater than or equal to
the value on the right hand side.}"
"@named{-}{The column on the left hand size must be less than or equal to th
e value on the right hand side.}"
"@named{~}{The column on the left hand side must contain the string on the r
ight hand side.}"
"@named{^}{The column on the left hand side must not contain string value on
the right hand side.}"
"\n\n"
"Commit hashes can be abbreviated in equal, unequal, less, and greater const
raints, even if the hash as a "
"\"+local\" suffix.\n\n"
"Dates can be entred as \"YYYYMMDD\" or \"YYYY-MM-DD\" and match all time po
ints within that day. Times are "
"specified as a date followed by \"HHMMSS\" or \"HH:MM:SS\". The time can be
separated from the date with "
"white space or the letter \"T\". A time (or date alone) can be followed by
a timezone abbreviation or the "
"time standard \"UTC\". Time zones and standards can be all upper- or all lo
wer-case and separated from the "
"time (or date only) by white space. If no time zone or standard is specifie
d, then either GMT or your "
"local time zone is assumed, depending on whether %s{localtime} was specifie
d. For example, today is "
"\"2021-10-09\", \"20211009\", or \"20211009 UTC\", and the current time is
\"20211009T190424\" (ISO 8601 "
"format) or \"2021-10-09 15:04:24 EDT\" in a more human friendly format.\n\n
"
"A display column can be sorted by appending \".a\" or \".d\" to its name, m
eaning sort so values are ascending "
"or descending, respectively. If multiple columns are sorted, their sort pri
ority is based on their column number. "
"That is, the left-most sorted column is sorted first, then the next column
to the right, etc.";
#include <rose.h> #include <rose.h>
#include "matrixTools.h" #include "matrixTools.h"
#include <Rose/CommandLine.h> #include <Rose/CommandLine.h>
#include <Rose/FormattedTable.h> #include <Rose/FormattedTable.h>
#include <Sawyer/Database.h> #include <Sawyer/Database.h>
#include <Sawyer/Map.h> #include <Sawyer/Map.h>
#include <boost/any.hpp> #include <boost/any.hpp>
#include <boost/format.hpp> #include <boost/format.hpp>
using namespace Rose; using namespace Rose;
using namespace Sawyer::Message::Common; using namespace Sawyer::Message::Common;
namespace DB = Sawyer::Database; namespace DB = Sawyer::Database;
struct Settings { struct Settings {
bool usingLocalTime = false; bool usingLocalTime = false;
std::string sortField;
Format outputFormat = Format::PLAIN; Format outputFormat = Format::PLAIN;
std::string databaseUri; // e.g., postgresql://us er:password@host/database std::string databaseUri; // e.g., postgresql://us er:password@host/database
Sawyer::Optional<size_t> limit; // limit number of resul
ting rows
bool deleteMatchingTests = false; // if true, delete the t
ests whose records match
bool showAges = true; // when showing times, a
lso say "about x days ago" or similar
bool considerAll = false; // consider all tests in
stead of just the latest ROSE version
}; };
static Sawyer::Message::Facility mlog; static Sawyer::Message::Facility mlog;
static const size_t DEFAULT_ROW_LIMIT = 100;
static std::vector<std::string> static std::vector<std::string>
parseCommandLine(int argc, char *argv[], Settings &settings) { parseCommandLine(int argc, char *argv[], Settings &settings) {
using namespace Sawyer::CommandLine; using namespace Sawyer::CommandLine;
Parser parser = Rose::CommandLine::createEmptyParser(gPurpose, gDescription) ; Parser parser = Rose::CommandLine::createEmptyParser(gPurpose, gDescription) ;
parser.errorStream(mlog[FATAL]); parser.errorStream(mlog[FATAL]);
parser.doc("Synopsis", "@prop{programName} [@v{switches}] @v{columns}"); parser.doc("Synopsis", "@prop{programName} [@v{switches}] @v{columns}");
SwitchGroup sg("Tool-specific switches"); SwitchGroup sg("Tool-specific switches");
insertDatabaseSwitch(sg, settings.databaseUri); insertDatabaseSwitch(sg, settings.databaseUri);
insertOutputFormatSwitch(sg, settings.outputFormat); insertOutputFormatSwitch(sg, settings.outputFormat,
FormatFlags()
.set(Format::PLAIN)
.set(Format::YAML)
.set(Format::HTML)
.set(Format::CSV)
.set(Format::SHELL));
sg.insert(Switch("all", 'a')
.intrinsicValue(true, settings.considerAll)
.doc("Consider all tests instead of just the latest ROSE version.
Constraining the \"rose\" or \"rose_date\" "
"columns also implies @s{all}."));
sg.insert(Switch("localtime") sg.insert(Switch("localtime")
.intrinsicValue(true, settings.usingLocalTime) .intrinsicValue(true, settings.usingLocalTime)
.doc("Display and parse times using the local time zone. Warning: parsing local times uses the daylight " .doc("Display and parse times using the local time zone. Warning: parsing local times uses the daylight "
"saving time mode as of the time of parsing, and parsing of t imes near the switch between standard " "saving time mode as of the time of parsing, and parsing of t imes near the switch between standard "
"and daylight saving time can be ambiguous. The default is to use " + "and daylight saving time can be ambiguous. The default is to use " +
std::string(settings.usingLocalTime ? "local time" : "GMT") + ".")); std::string(settings.usingLocalTime ? "local time" : "GMT") + "."));
sg.insert(Switch("sort") sg.insert(Switch("limit", 'n')
.argument("field", anyParser(settings.sortField)) .argument("nrows", nonNegativeIntegerParser(settings.limit))
.doc("Sort the output according to the specified column. The colum .doc("Limit the number of rows returned by the query. The default
n need not be a column that's being " is " +
"displayed in the output. The sort is always increasing.")); boost::lexical_cast<std::string>(DEFAULT_ROW_LIMIT) + "."));
sg.insert(Switch("delete")
.intrinsicValue(true, settings.deleteMatchingTests)
.doc("Delete the tests that were matched."));
Rose::CommandLine::insertBooleanSwitch(sg, "show-age", settings.showAges,
"Causes timestamps to also incude an
approximate age. For instance, the "
"age might be described as \"about 6
hours ago\".");
return parser return parser
.with(Rose::CommandLine::genericSwitches()) .with(Rose::CommandLine::genericSwitches())
.with(sg) .with(sg)
.parse(argc, argv) .parse(argc, argv)
.apply() .apply()
.unreachedArgs(); .unreachedArgs();
} }
using DependencyNames = Sawyer::Container::Map<std::string /*key*/, std::string enum class ColumnType {
/*colname*/>; STRING,
INTEGER,
TIME,
DURATION,
VERSION_OR_HASH
};
enum class Sorted {
NONE,
ASCENDING,
DESCENDING
};
struct Binding {
std::string name;
std::string value;
Binding() {}
Binding(const std::string &name, const std::string &value)
: name(name), value(value) {}
};
// Information about database columns
class Column {
// Declaration stuff
std::string tableTitle_; // title to use in the t
able column
std::string key_; // name that appears on
the command-line
std::string identifier_; // like key, but suitabl
e as an identifier in YAML or SQL
size_t sqlSuffix_ = 0; // suffix for SQL aliase
s
std::string sql_; // the value as written
in SQL
std::string doc_; // description
ColumnType type_ = ColumnType::STRING; // type of value returne
d by the sqlExpression
bool isAggregate_ = false; // the column value is a
ggregated over the selection group
// Result stuff
bool isDisplayed_ = false; // should we show this c
olumn in the results?
std::string constraintExpr_; // part of a WHERE claus
e
std::vector<Binding> constraintBindings_; // values that need to b
e bound to the constraint
Sorted sorted_ = Sorted::NONE; // how to sort a display
ed result
public:
Column() {}
const std::string& tableTitle() const {
return tableTitle_;
}
Column& tableTitle(const std::string &s) {
tableTitle_ = s;
return *this;
}
const std::string& key() const {
return key_;
}
Column& key(const std::string &s) {
key_ = s;
return *this;
}
const std::string& identifier() const {
return identifier_;
}
Column& identifier(const std::string &s) {
identifier_ = s;
return *this;
}
const std::string& sql() const {
return sql_;
}
Column& sql(const std::string &s) {
sql_ = s;
return *this;
}
Column& sqlSuffix(size_t n) {
sqlSuffix_ = n;
return *this;
}
std::string sqlAlias() const {
return identifier_ + boost::lexical_cast<std::string>(sqlSuffix_);
}
const std::string& doc() const {
return doc_;
}
Column& doc(const std::string &s) {
doc_ = s;
return *this;
}
ColumnType type() const {
return type_;
}
Column& type(ColumnType t) {
type_ = t;
return *this;
}
bool isAggregate() const {
return isAggregate_;
}
Column& isAggregate(bool b) {
isAggregate_ = b;
return *this;
}
const std::string& constraintExpr() const {
return constraintExpr_;
}
Column& constraintExpr(const std::string &s) {
constraintExpr_ = s;
return *this;
}
const std::vector<Binding>& constraintBindings() const {
return constraintBindings_;
}
Column& constraintBinding(const std::string &name, const std::string &value)
{
constraintBindings_.push_back(Binding(name, value));
return *this;
}
template<typename T>
Column& constraintBinding(const std::string &name, const T& value) {
constraintBindings_.push_back(Binding(name, boost::lexical_cast<std::str
ing>(value)));
return *this;
}
bool isDisplayed() const {
return isDisplayed_;
}
Column& isDisplayed(bool b) {
isDisplayed_ = b;
return *this;
}
// Does this column appear in the list of columns after SELECT?
bool isSelected() const {
return isDisplayed_ || constraintExpr_.empty();
}
static DependencyNames Sorted sorted() const {
loadDependencyNames(DB::Connection db) { return sorted_;
DependencyNames retval; }
auto stmt = db.stmt("select distinct name from dependencies"); Column& sorted(Sorted s) {
sorted_ = s;
return *this;
}
};
// A mapping from command-line name (key) to database column
using ColumnMap = Sawyer::Container::Map<std::string, Column>;
// List of columns
using ColumnList = std::vector<Column>;
// Create a map that declares all the possible database columns that can be quer
ied.
static ColumnMap
loadColumns(DB::Connection db) {
ColumnMap retval;
// Columns that are ROSE dependencies
auto stmt = db.stmt("select name, column_name, description from dependency_a
ttributes");
for (auto row: stmt) { for (auto row: stmt) {
const std::string key = *row.get<std::string>(0); const std::string key = *row.get<std::string>(0);
retval.insert(key, "rmc_" + key); const std::string columnName = *row.get<std::string>(1);
auto doc = row.get<std::string>(2);
Column c;
// Default values
std::string s = key;
s[0] = toupper(s[0]);
c.tableTitle(s);
c.sql("test_results." + columnName);
c.type(ColumnType::STRING);
c.doc(doc.orElse("The " + key + " dependency"));
// Specific values for some columns
if ("build" == key) {
c.tableTitle("Build\nSystem");
} else if ("edg_compile" == key) {
c.tableTitle("EDG\nCompile");
}
retval.insert(key, c);
} }
// Additional key/column relationships // Other columns
retval.insert("id", "test.id"); retval.insert("count",
retval.insert("reporting_user", "auth_user.identity"); Column().tableTitle("Count").sql("count(*)")
retval.insert("reporting_time", "test.reporting_time"); .doc("Number of tests represented by each row of the result ta
retval.insert("tester", "test.tester"); ble")
retval.insert("os", "test.os"); .type(ColumnType::INTEGER)
retval.insert("rose", "test.rose"); .isAggregate(true));
retval.insert("rose_date", "test.rose_date"); retval.insert("duration",
retval.insert("status", "test.status"); Column().tableTitle("Test\nDuration").sql("test_results.durati
retval.insert("duration", "test.duration"); on")
retval.insert("noutput", "test.noutput"); .doc("How long the test took not counting slave setup")
retval.insert("nwarnings", "test.nwarnings"); .type(ColumnType::DURATION));
retval.insert("first_error", "test.first_error"); retval.insert("duration.avg",
retval.insert("count", "count"); Column().tableTitle("Average\nDuration").sql("avg(test_results
.duration)")
.doc("Average length of time taken for the selected tests")
.type(ColumnType::DURATION)
.isAggregate(true));
retval.insert("duration.max",
Column().tableTitle("Max\nDuration").sql("max(test_results.dur
ation)")
.doc("Maximum length of time taken for any selected test")
.type(ColumnType::DURATION)
.isAggregate(true));
retval.insert("duration.min",
Column().tableTitle("Min\nDuration").sql("min(test_results.dur
ation)")
.doc("Minimum length of time taken for any selected test")
.type(ColumnType::DURATION)
.isAggregate(true));
retval.insert("first_error",
Column().tableTitle("First Error").sql("test_results.first_err
or")
.doc("Heuristically detected first error message")
.type(ColumnType::STRING));
retval.insert("id",
Column().tableTitle("ID").sql("test_results.id")
.doc("Test ID number")
.type(ColumnType::INTEGER));
retval.insert("os",
Column().tableTitle("Operating\nSystem").sql("test_results.os"
)
.doc("Operating system")
.type(ColumnType::STRING));
retval.insert("output",
Column().tableTitle("Lines of\nOutput").sql("test_results.nout
put")
.doc("Number of lines of output")
.type(ColumnType::INTEGER));
retval.insert("output.avg",
Column().tableTitle("Avg Lines\nof Output").sql("avg(test_resu
lts.noutput)")
.doc("Average number of lines of output for the selected tests
")
.type(ColumnType::INTEGER)
.isAggregate(true));
retval.insert("output.max",
Column().tableTitle("Max Lines\nof Output").sql("max(test_resu
lts.noutput)")
.doc("Maximum number of lines of output for any selected test"
)
.type(ColumnType::INTEGER)
.isAggregate(true));
retval.insert("output.min",
Column().tableTitle("Min Lines\nof Output").sql("min(test_resu
lts.noutput)")
.doc("Minimum number of lines of output for any selected test"
)
.type(ColumnType::INTEGER)
.isAggregate(true));
retval.insert("pf",
Column().tableTitle("P/F")
.sql("case when status = 'end' then 'pass' else 'fail' end")
.doc("The word \"pass\" if status is \"end\", otherwise the wo
rd \"fail\"")
.type(ColumnType::STRING));
retval.insert("reporting_time",
Column().tableTitle("Reporting Time").sql("test_results.report
ing_time")
.doc("Time at which results were reported")
.type(ColumnType::TIME));
retval.insert("reporting_time.max",
Column().tableTitle("Latest Report").sql("max(test_results.rep
orting_time)")
.doc("Latest time at which results were reported")
.type(ColumnType::TIME)
.isAggregate(true));
retval.insert("reporting_time.min",
Column().tableTitle("Earliest Report").sql("min(test_results.r
eporting_time)")
.doc("Earliest time at which results were reported")
.type(ColumnType::TIME)
.isAggregate(true));
retval.insert("reporting_user",
Column().tableTitle("Reporting User").sql("auth_identities.ide
ntity")
.doc("User that reported the results")
.type(ColumnType::STRING));
retval.insert("rose",
Column().tableTitle("ROSE\nVersion").sql("test_results.rose")
.doc("ROSE version number or commit hash")
.type(ColumnType::VERSION_OR_HASH));
retval.insert("rose.max",
Column().tableTitle("Max ROSE\nVersion").sql("max(test_results
.rose)")
.doc("Maximum ROSE version number or commit hash")
.type(ColumnType::VERSION_OR_HASH)
.isAggregate(true));
retval.insert("rose.min",
Column().tableTitle("Min ROSE\nVersion").sql("min(test_results
.rose)")
.doc("Minimum ROSE version number or commit hash")
.type(ColumnType::VERSION_OR_HASH)
.isAggregate(true));
retval.insert("rose_date",
Column().tableTitle("ROSE Date").sql("test_results.rose_date")
.doc("ROSE version or commit date")
.type(ColumnType::TIME));
retval.insert("rose_date.max",
Column().tableTitle("Latest\nROSE Date").sql("max(test_results
.rose_date)")
.doc("Latest ROSE version or commit date")
.type(ColumnType::TIME)
.isAggregate(true));
retval.insert("rose_date.min",
Column().tableTitle("Earliest\nROSE Date").sql("min(test_resul
ts.rose_date)")
.doc("Earliest ROSE version or commit date")
.type(ColumnType::TIME)
.isAggregate(true));
retval.insert("slave",
Column().tableTitle("Slave Name").sql("test_results.tester")
.doc("Name of testing slave")
.type(ColumnType::STRING));
retval.insert("status",
Column().tableTitle("Status").sql("test_results.status")
.doc("Result status of test; phase at which test failed")
.type(ColumnType::STRING));
retval.insert("warnings",
Column().tableTitle("Number of\nWarnings").sql("test_results.n
warnings")
.doc("Number of heuristically detected warning messages")
.type(ColumnType::INTEGER));
retval.insert("warnings.avg",
Column().tableTitle("Average\nWarnings").sql("avg(test_results
.nwarnings)")
.doc("Average number of heuristically detected warning message
s for the selected tests")
.type(ColumnType::INTEGER)
.isAggregate(true));
retval.insert("warnings.max",
Column().tableTitle("Max\nWarnings").sql("max(test_results.nwa
rnings)")
.doc("Maximum number of heuristically detected warning message
s for any selected tests")
.type(ColumnType::INTEGER)
.isAggregate(true));
retval.insert("warnings.min",
Column().tableTitle("Min\nWarnings").sql("min(test_results.nwa
rnings)")
.doc("Minimum number of heuristically detected warning message
s for any selected tests")
.type(ColumnType::INTEGER)
.isAggregate(true));
for (ColumnMap::Node &node: retval.nodes()) {
Column &c = node.value();
if (c.key().empty())
c.key(node.key());
if (c.identifier().empty())
c.identifier(boost::replace_all_copy(node.key(), ".", "_"));
}
return retval; return retval;
} }
static std::string
findLatestRoseVersion(DB::Connection db) {
return db.stmt("select rose from test_results order by reporting_time desc l
imit 1").get<std::string>().orElse("");
}
static Sawyer::Optional<std::pair<time_t, time_t> > static Sawyer::Optional<std::pair<time_t, time_t> >
parseDateTime(const Settings &settings, std::string s) { parseDateTime(const Settings &settings, std::string s) {
struct tm tmMin, tmMax; struct tm tmMin, tmMax;
boost::smatch matches; boost::smatch matches;
time_t now = 0; time_t now = 0;
time(&now); time(&now);
localtime_r(&now, &tmMin); localtime_r(&now, &tmMin);
tmMax = tmMin; tmMax = tmMin;
skipping to change at line 135 skipping to change at line 490
boost::regex_match(s, matches, timeRe2)) { boost::regex_match(s, matches, timeRe2)) {
tmMin.tm_hour = tmMax.tm_hour = boost::lexical_cast<int>(matches[1].str( )); tmMin.tm_hour = tmMax.tm_hour = boost::lexical_cast<int>(matches[1].str( ));
tmMin.tm_min = tmMax.tm_min = boost::lexical_cast<int>(matches[2].str( )); tmMin.tm_min = tmMax.tm_min = boost::lexical_cast<int>(matches[2].str( ));
tmMin.tm_sec = tmMax.tm_sec = boost::lexical_cast<int>(matches[3].str( )); tmMin.tm_sec = tmMax.tm_sec = boost::lexical_cast<int>(matches[3].str( ));
s = matches[4].str(); s = matches[4].str();
} else { } else {
// time is optional // time is optional
} }
bool usingLocalTime = true; bool usingLocalTime = true;
boost::regex utcRe("\\s*(z|Z|u|U|utc|UTC)"); boost::regex utcRe("\\s*(z|Z|u|U|utc|UTC|gmt|GMT)");
if (s.empty()) { if (s.empty()) {
usingLocalTime = settings.usingLocalTime; usingLocalTime = settings.usingLocalTime;
} else if (boost::regex_match(s, utcRe)) { } else if (boost::regex_match(s, utcRe)) {
usingLocalTime = false; usingLocalTime = false;
} else { } else {
// zone is not supported // zone is not supported
return Sawyer::Nothing(); return Sawyer::Nothing();
} }
time_t tmin, tmax; time_t tmin, tmax;
skipping to change at line 158 skipping to change at line 513
tmax = timelocal(&tmMax); tmax = timelocal(&tmMax);
} else { } else {
tmMin.tm_isdst = tmMax.tm_isdst = 0; tmMin.tm_isdst = tmMax.tm_isdst = 0;
tmin = timegm(&tmMin); tmin = timegm(&tmMin);
tmax = timegm(&tmMax); tmax = timegm(&tmMax);
} }
return std::make_pair(tmin, tmax); return std::make_pair(tmin, tmax);
} }
int static std::string
main(int argc, char *argv[]) { sqlComparison(const std::string &s) {
ROSE_INITIALIZE; if ("=" == s) {
Diagnostics::initAndRegister(&mlog, "tool"); return " = ";
} else if ("/" == s) {
Settings settings; return " <> ";
std::vector<std::string> args = parseCommandLine(argc, argv, settings); } else if ("~" == s) {
auto db = DB::Connection::fromUri(settings.databaseUri); return " like ";
DependencyNames dependencyNames = loadDependencyNames(db); } else if ("^" == s) {
return " not like ";
// Parse positional command-line arguments } else if ("-" == s) {
boost::regex nameRe("[_a-zA-Z][_a-zA-Z0-9]*"); return " <= ";
boost::regex exprRe("([_a-zA-Z][_a-zA-Z0-9]*)([=~])(.*)"); } else if ("+" == s) {
Sawyer::Container::Set<std::string> keysSeen; return " >= ";
std::vector<std::string> whereClauses, columnsSelected, keysSelected; } else {
Sawyer::Container::Map<std::string, boost::any> whereValues; ASSERT_not_reachable("unknown comparison operator \"" + s + "\"");
for (const std::string &arg: args) { }
boost::smatch exprParts; }
if (boost::regex_match(arg, exprParts, exprRe)) {
// Arguments of the form <KEY><OPERATOR><VALUE> mean restrict the ta
ble to that value of the key. This key column
// will be emitted above the table instead of within the table (sinc
e the column within the table would have one
// value across all the rows.
std::string key = exprParts.str(1);
std::string comparison = exprParts.str(2);
std::string val = exprParts.str(3);
if (!dependencyNames.exists(key)) { // Adjust the column so it's constrained as specified by the comparison and valu
mlog[FATAL] <<"invalid key \"" <<StringUtility::cEscape(key) <<" e. The key is the name of the column
\"\n"; // from the command-line and is used in error messages.
static void
constrainColumn(const Settings &settings, Column &c, const std::string &key, con
st std::string &comparison,
const std::string &value) {
switch (c.type()) {
case ColumnType::VERSION_OR_HASH: {
boost::regex partialKeyRe("([0-9a-f]{1,39})(\\+local)?");
boost::smatch found;
if (boost::regex_match(value, found, partialKeyRe)) {
// Special cases for comparing ROSE abbreviated commit hashes
const std::string abbrHash = found.str(1);
const std::string local = found.str(2);
if ("=" == comparison || "/" == comparison) {
const std::string sqlOp = "=" == comparison ? " like " : " n
ot like ";
c.constraintExpr(c.sql() + sqlOp + "?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), abbrHash + "%" + local);
break;
} else if ("-" == comparison) {
const std::string fullHash = abbrHash + std::string(40-abbrH
ash.size(), 'f') + local;
c.constraintExpr(c.sql() + " <= ?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), fullHash);
break;
} else if ("+" == comparison) {
const std::string fullHash = abbrHash + std::string(40-abbrH
ash.size(), '0') + local;
c.constraintExpr(c.sql() + " >= ?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), fullHash);
break;
}
}
}
// fall through to string comparison
case ColumnType::STRING:
if ("=" == comparison || "/" == comparison || "-" == comparison || "
+" == comparison) {
const std::string sqlOp = sqlComparison(comparison);
c.constraintExpr(c.sql() + sqlOp + "?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), value);
} else if ("~" == comparison || "^" == comparison) {
const std::string sqlOp = sqlComparison(comparison);
c.constraintExpr(c.sql() + sqlOp + "?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), "%" + value + "%");
} else {
mlog[FATAL] <<"field \"" <<key <<"\" cannot be constrained with
\"" <<comparison <<"\" operator\n";
exit(1); exit(1);
} }
if ("count" == key) { break;
mlog[FATAL] <<"field \"count\" cannot be compared\n";
case ColumnType::INTEGER:
if ("=" == comparison || "/" == comparison || "-" == comparison || "
+" == comparison) {
const std::string sqlOp = sqlComparison(comparison);
c.constraintExpr(c.sql() + sqlOp + "?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), value);
} else {
mlog[FATAL] <<"field \"" <<key <<"\" cannot be constrained with
\"" <<comparison <<"\" operator\n";
exit(1); exit(1);
} }
break;
if ("rose" == key && "=" == comparison) { case ColumnType::TIME:
// Special cases for comparing ROSE commit hashes to allow speci // Allow for ranges of time to be used when only a date is specified
fying abbreviated hashes. if ("=" == comparison || "/" == comparison || "-" == comparison || "
boost::regex partialKeyRe("([0-9a-f]{1,39})(\\+local)?"); +" == comparison) {
boost::smatch matches; if (Sawyer::Optional<std::pair<time_t, time_t>> range = parseDat
if (boost::regex_match(val, matches, partialKeyRe)) { eTime(settings, value)) {
whereClauses.push_back(dependencyNames[key] + " like ?rose") if ("=" == comparison) {
; c.constraintExpr(c.sql() + " >= ?min_" + c.sqlAlias() +
val = matches[1].str() + "%" + matches[2].str(); " and " +
c.sql() + " <= ?max_" + c.sqlAlias());
c.constraintBinding("min_" + c.sqlAlias(), range->first)
;
c.constraintBinding("max_" + c.sqlAlias(), range->second
);
} else if ("/" == comparison) {
c.constraintExpr(c.sql() + " < ?min_" + c.sqlAlias() + "
or " +
c.sql() + " > ?max_" + c.sqlAlias());
c.constraintBinding("min_" + c.sqlAlias(), range->first)
;
c.constraintBinding("max_" + c.sqlAlias(), range->second
);
} else if ("-" == comparison) {
c.constraintExpr(c.sql() + " <= ?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), range->second);
} else {
ASSERT_require("+" == comparison);
c.constraintExpr(c.sql() + " >= ?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), range->first);
}
} else { } else {
whereClauses.push_back(dependencyNames[key] + " = ?rose"); mlog[FATAL] <<"invalid date/time value in constraint for \""
} <<key <<"\": "
whereValues.insert(key, val); <<"\"" <<StringUtility::cEscape(value) <<"\"\n";
} else if ("reporting_time" == key || "rose_date" == key) {
// Special case for dates to allow for ranges.
if (comparison != "=") {
mlog[FATAL] <<"field \"" <<key <<"\" can only be compared wi
th \"=\"\n";
exit(1); exit(1);
} }
if (Sawyer::Optional<std::pair<time_t, time_t> > range = parseDa } else {
teTime(settings, val)) { mlog[FATAL] <<"field \"" <<key <<"\" cannot be constrained with
whereClauses.push_back(dependencyNames[key] + " >= ?min_" + \"" <<comparison <<"\" operator\n";
key + " and " + exit(1);
dependencyNames[key] + " <= ?max_" + }
key); break;
whereValues.insert("min_" + key, range->first);
whereValues.insert("max_" + key, range->second);
} else {
whereClauses.push_back(dependencyNames[key] + " = ?" + key);
whereValues.insert(key, val);
}
} else if ("~" == comparison) {
// Substring comparison
whereClauses.push_back(dependencyNames[key] + " like ?" + key);
whereValues.insert(key, "%" + val + "%");
case ColumnType::DURATION:
if ("=" == comparison || "/" == comparison || "-" == comparison || "
+" == comparison) {
const std::string sqlOp = sqlComparison(comparison);
c.constraintExpr(c.sql() + sqlOp + "?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), Rose::CommandLine::DurationPar
ser::parse(value));
} else { } else {
// Equality comparison mlog[FATAL] <<"field \"" <<key <<"\" cannot be constrained with
whereClauses.push_back(dependencyNames[key] + " = ?" + key); \"" <<comparison <<"\" operator\n";
whereValues.insert(key, val); exit(1);
} }
break;
}
}
std::cerr <<" " <<std::left <<std::setw(16) <<key <<" = \"" <<Strin // Number of columns being displayed
gUtility::cEscape(val) <<"\"\n"; static size_t
keysSeen.insert(key); nDisplayed(const ColumnList &cols) {
size_t retval = 0;
} else if (arg == "list") { for (const Column &c: cols) {
std::cout <<"Column names:\n"; if (c.isDisplayed())
for (const std::string &key: dependencyNames.keys()) ++retval;
std::cout <<" " <<key <<"\n"; }
exit(0); return retval;
}
} else if (arg == "all") { // Is the specified column alread selected for displaying in the results?
for (const DependencyNames::Node &node: dependencyNames.nodes()) { static bool
if (!keysSeen.exists(node.key())) { isDisplayed(const ColumnList &cols, const std::string &key) {
keysSelected.push_back(node.key()); for (const Column &c: cols) {
columnsSelected.push_back(node.value()); if (c.key() == key && c.isDisplayed())
keysSeen.insert(node.key()); return true;
} }
return false;
}
// Find the column in the list of columns and return its index if it is present
static Sawyer::Optional<size_t>
find(const ColumnList &cols, const std::string &key) {
for (size_t i = 0; i < cols.size(); ++i) {
if (cols[i].key() == key)
return i;
}
return Sawyer::Nothing();
}
static Column&
appendColumn(ColumnList &cols, const Column &col) {
static Sawyer::Container::Map<std::string /*column_key*/, size_t /*count*/>
suffixes;
cols.push_back(col);
cols.back().sqlSuffix(++suffixes.insertMaybe(col.key(), 0));
return cols.back();
}
static void
showConstraint(const std::string &name, const std::string &comparison, const std
::string &value) {
SAWYER_MESG(mlog[INFO]) <<(boost::format(" %-16s %s \"%s\"\n") % name % com
parison % StringUtility::cEscape(value));
}
// Constrain ROSE version if appropriate
static void
maybeConstrainRoseVersion(const Settings &settings, DB::Connection db, ColumnLis
t &cols, const ColumnMap &decls) {
if (settings.considerAll)
return;
for (const Column &c: cols) {
if ((c.key() == "rose" || c.key() == "rose_date") && !c.constraintExpr()
.empty())
return;
}
std::string roseVersion = findLatestRoseVersion(db);
if (!roseVersion.empty()) {
Column &c = appendColumn(cols, decls["rose"]);
c.constraintExpr(c.sql() + " = ?" + c.sqlAlias());
c.constraintBinding(c.sqlAlias(), roseVersion);
showConstraint("rose", "=", roseVersion);
}
}
// Display all columns that we haven't chosen to display yet and which don't hav
e constraints
static void
displayMoreColumns(ColumnList &cols, const ColumnMap &decls) {
for (const Column &decl: decls.values()) {
if (!decl.isAggregate()) {
if (auto idx = find(cols, decl.key())) {
if (cols[*idx].constraintExpr().empty())
cols[*idx].isDisplayed(true);
} else {
appendColumn(cols, decl).isDisplayed(true);
} }
}
}
}
} else if (!dependencyNames.exists(arg)) { // Convert the list of displayed columns to an SQL column list for a SELECT stat
// Arguments of the form "key" mean add that key as one of the table ement.
columns and sort the table by ascending static std::string
// values. buildSelectClause(const ColumnList &cols) {
mlog[FATAL] <<"invalid key \"" <<StringUtility::cEscape(arg) <<"\"\n std::string s;
"; for (const Column &c: cols) {
exit(1); if (c.isSelected()) {
s += (s.empty() ? "select " : ", ") + c.sql();
if (c.sql() != c.sqlAlias())
s += " as " + c.sqlAlias();
}
}
return s;
}
} else { // Build the GROUP BY clause
keysSelected.push_back(arg); static std::string
columnsSelected.push_back(dependencyNames[arg]); buildGroupByClause(const ColumnList &cols) {
keysSeen.insert(arg); std::string s;
for (const Column &c: cols) {
if (c.isDisplayed() && !c.isAggregate())
s += (s.empty() ? " group by " : ", ") + c.sqlAlias();
}
return s;
}
// Build an optional WHERE clause for the constrainted columns
static std::string
buildWhereClause(const ColumnList &cols) {
std::string s;
for (const Column &c: cols) {
if (!c.constraintExpr().empty() && !c.isAggregate())
s += (s.empty() ? " where " : " and ") + c.constraintExpr();
}
return s;
}
// Build an optional HAVING clause for the constrained aggregate columns
static std::string
buildHavingClause(const ColumnList &cols) {
std::string s;
for (const Column &c: cols) {
if (!c.constraintExpr().empty() && c.isAggregate())
s += (s.empty() ? " having " : " and ") + c.constraintExpr();
}
return s;
}
// Build an optional ORDERED BY clause
static std::string
buildOrderedByClause(const ColumnList &cols) {
std::string s;
for (const Column &c: cols) {
if (c.isDisplayed() && c.sorted() != Sorted::NONE) {
s += (s.empty() ? " order by " : ", ") + c.sqlAlias();
if (Sorted::DESCENDING == c.sorted())
s += " desc";
} }
} }
return s;
}
// If no columns are selected, then select lots of them // Add variable bindings for constraints
if (keysSelected.empty()) { static void
for (const DependencyNames::Node &node: dependencyNames.nodes()) { buildConstraintBindings(DB::Statement stmt, const ColumnList &cols) {
if (!keysSeen.exists(node.key())) { for (const Column &c: cols) {
keysSelected.push_back(node.key()); for (const Binding &b: c.constraintBindings()) {
columnsSelected.push_back(node.value()); stmt.bind(b.name, b.value);
} SAWYER_MESG(mlog[DEBUG]) <<" where " <<b.name <<" = " <<b.value <<"
\n";
} }
} }
}
// Build the SQL statement. In order to suppress the "count" from the output static DB::Statement
and yet use it in the ORDER BY clause buildStatement(const Settings &settings, DB::Connection db, const ColumnList &co
// we need to have a two-level query that follows the format: ls) {
// select SET1 from (select SET2, count(*) as count from ....) as tbl; std::string sql = buildSelectClause(cols) +
// where SET1 is the list of column names provided by the user " from test_results" +
// where SET2 is SET1 - "count" " join auth_identities on test_results.reporting_user = au
std::string sql = "select"; th_identities.id" +
if (columnsSelected.empty()) { buildWhereClause(cols) +
sql += " *"; buildGroupByClause(cols) +
buildHavingClause(cols) +
buildOrderedByClause(cols);
if (settings.limit) {
sql += " limit " + boost::lexical_cast<std::string>(*settings.limit);
} else { } else {
for (size_t i = 0; i < columnsSelected.size(); ++i) { // +1 is so we can detect if there are more rows
std::string name = boost::replace_all_copy(columnsSelected[i], ".", sql += " limit " + boost::lexical_cast<std::string>(DEFAULT_ROW_LIMIT +
"_"); 1);
sql += std::string(i?",":"") + " " + name; }
SAWYER_MESG(mlog[DEBUG]) <<"SQL: " <<sql <<"\n";
DB::Statement stmt = db.stmt(sql);
buildConstraintBindings(stmt, cols);
return stmt;
}
static void
buildTableColumnHeaders(FormattedTable &table, const ColumnList &cols) {
size_t j = 0;
for (const Column &c: cols) {
if (c.isDisplayed())
table.columnHeader(0, j++, c.tableTitle());
}
}
static std::string
formatValue(const Settings &settings, const Column &c, const std::string &value)
{
if (Format::SHELL == settings.outputFormat)
return value;
switch (c.type()) {
case ColumnType::STRING:
case ColumnType::INTEGER:
case ColumnType::VERSION_OR_HASH:
return value;
case ColumnType::TIME: {
time_t when = boost::lexical_cast<time_t>(value);
std::string s = settings.usingLocalTime ? timeToLocal(when) : timeTo
Gmt(when);
if (settings.showAges)
s += ", " + approximateAge(when);
return s;
}
case ColumnType::DURATION: {
uint64_t length = 0;
try {
length = boost::lexical_cast<uint64_t>(value);
} catch (...) {
length = ::round(boost::lexical_cast<double>(value));
}
return Rose::CommandLine::DurationParser::toString(length);
} }
} }
sql += " from (select"; }
if (columnsSelected.empty()) {
sql += " *"; // Returns the number of tests matched by the row (zero if unavailable)
} else { static size_t
size_t nColsEmitted = 0; emitTable(const Settings &settings, FormattedTable &table, DB::Row &row, const C
for (size_t i = 0; i < columnsSelected.size(); ++i) { olumnList &cols,
std::string alias = boost::replace_all_copy(columnsSelected[i], ".", std::set<size_t> &testIds /*out*/) {
"_"); const size_t i = table.nRows();
if (columnsSelected[i] != "count") size_t j = 0, nTests = 0;
sql += std::string(nColsEmitted++?",":"") + " " + columnsSelecte for (const Column &c: cols) {
d[i] + " as " + alias; if (c.isDisplayed()) {
} if (auto value = row.get<std::string>(j))
sql += std::string(nColsEmitted?",":"") + " count(*) as count"; table.insert(i, j, formatValue(settings, c, *value));
} }
sql += " from test_results as test join auth_identities as auth_user on test
.reporting_user = auth_user.id"; if (c.isSelected()) {
if (!whereClauses.empty()) if (c.key() == "count") {
sql += " where " + StringUtility::join(" and ", whereClauses); nTests += row.get<size_t>(j).orElse(0);
if (!columnsSelected.empty()) { } else if (c.key() == "id") {
for (size_t i=0, nEmit=0; i < columnsSelected.size(); ++i) { testIds.insert(*row.get<size_t>(j));
if (columnsSelected[i] != "count") }
sql += std::string(nEmit++?", ":" group by ") + columnsSelected[ ++j;
i];
}
}
if (!settings.sortField.empty()) {
std::string sortBy;
if (dependencyNames.getOptional(settings.sortField).assignTo(sortBy)) {
sql += " order by " + sortBy;
} else {
mlog[FATAL] <<"cannot sort by \"" <<StringUtility::cEscape(settings.
sortField) <<"\": not a vaild field name\n";
exit(1);
} }
} }
sql += ") as tbl"; return nTests;
}
auto query = db.stmt(sql); // Returns the number of tests matched by the row (zero if unavailable)
for (auto &node: whereValues.nodes()) { static size_t
if (node.value().type() == typeid(std::string)) { emitYaml(const Settings &settings, DB::Row &row, const ColumnList &cols, std::se
query.bind(node.key(), boost::any_cast<std::string>(node.value())); t<size_t> &testIds /*out*/) {
} else if (node.value().type() == typeid(time_t)) { size_t j = 0, nTests = 0;
query.bind(node.key(), boost::any_cast<time_t>(node.value())); for (const Column &c: cols) {
} else { if (c.isDisplayed()) {
ASSERT_not_reachable("type for " + node.key()); if (auto value = row.get<std::string>(j)) {
const std::string formatted = formatValue(settings, c, *value);
std::cout <<(0 == j ? "- " : " ") <<c.identifier() <<": " <<Str
ingUtility::yamlEscape(*value);
if (*value != formatted)
std::cout <<" # " <<formatted;
std::cout <<"\n";
}
}
if (c.isSelected()) {
if (c.key() == "count") {
nTests += row.get<size_t>(j).orElse(0);
} else if (c.key() == "id") {
testIds.insert(*row.get<size_t>(j));
}
++j;
} }
} }
return nTests;
}
// Run the query and save results so we can compute column sizes. static void
emitDeclarations(const Settings &settings, const ColumnMap &columnDecls) {
FormattedTable table; FormattedTable table;
for (size_t j = 0; j < keysSelected.size(); ++j) table.columnHeader(0, 0, "Command Line");
table.columnHeader(0, j, keysSelected[j]); table.columnHeader(0, 1, "Description");
for (auto row: query) { table.columnHeader(0, 2, "YAML key");
size_t i = table.nRows(); table.columnHeader(0, 3, "SQL expr");
for (size_t j = 0; j < columnsSelected.size(); ++j) { for (const Column &c: columnDecls.values()) {
std::string value; if (Format::YAML == settings.outputFormat) {
if ("test.rose_date" == columnsSelected[j] || std::cout <<"- key: " <<StringUtility::yamlEscape(c.key()) <<"\n";
"test.reporting_time" == columnsSelected[j]) { std::cout <<" yaml_key: " <<StringUtility::yamlEscape(c.identifier(
time_t t = row.get<unsigned long>(j).orElse(0); )) <<"\n";
struct tm tm; std::cout <<" sql: " <<StringUtility::yamlEscape(c.sql()) <<"\n";
std::string tz; std::cout <<" description: " <<StringUtility::yamlEscape(c.doc()) <
if (settings.usingLocalTime) { <"\n";
localtime_r(&t, &tm); } else {
tz = tm.tm_zone; const size_t i = table.nRows();
table.insert(i, 0, c.key());
table.insert(i, 1, c.doc());
table.insert(i, 2, c.identifier());
table.insert(i, 3, c.sql());
}
}
if (Format::YAML != settings.outputFormat) {
table.format(tableFormat(settings.outputFormat));
std::cout <<table;
}
}
int
main(int argc, char *argv[]) {
ROSE_INITIALIZE;
Diagnostics::initAndRegister(&mlog, "tool");
Settings settings;
std::vector<std::string> args = parseCommandLine(argc, argv, settings);
DB::Connection db = connectToDatabase(settings.databaseUri, mlog);
const ColumnMap columnDecls = loadColumns(db);
// Patterns
const std::string identifier = "(?:[a-zA-Z]\\w*)";
const std::string qualifier = "(?:\\.(?:avg|max|min))";
const std::string columnName = "(?:" + identifier + qualifier + "?)";
const std::string sort = "(?:\\.[ad])";
const std::string comparisonOperator = "[-+=/~^]";
const std::string value = ".*";
const boost::regex displayRe("(" + columnName + ")(" + sort + ")?");
const boost::regex expressionRe("(" + columnName + ")(" + comparisonOperator
+ ")(" + value + ")");
// Parse positional command-line positional arguments. They are either const
raints or column names to display.
std::vector<Column> cols;
for (const std::string &arg: args) {
boost::smatch exprParts;
if (boost::regex_match(arg, exprParts, expressionRe)) {
// Arguments of the form <KEY><OPERATOR><VALUE> constrain the result
s. This key column will be emitted above the
// table instead of within the table (since the column within the ta
ble would have one value across all the rows.
const std::string key = exprParts.str(1);
const std::string comparison = exprParts.str(2);
const std::string value = exprParts.str(3);
if (!columnDecls.exists(key)) {
mlog[FATAL] <<"invalid column \"" <<StringUtility::cEscape(key)
<<"\"\n"
<<"use \"list\" to get a list of valid column names\
n";
exit(1);
}
Column &c = appendColumn(cols, columnDecls[key]);
showConstraint(key, comparison, value);
constrainColumn(settings, c, key, comparison, value);
} else if ("list" == arg) {
// List all the column information
emitDeclarations(settings, columnDecls);
exit(0);
} else if ("all" == arg) {
displayMoreColumns(cols, columnDecls);
} else if (boost::regex_match(arg, exprParts, displayRe)) {
// A column name all by itself (or with a sort direction) means disp
lay the column
const std::string key = exprParts.str(1);
const std::string sorted = exprParts.str(2);
if (!columnDecls.exists(key)) {
mlog[FATAL] <<"invalid column \"" <<StringUtility::cEscape(key)
<<"\"\n"
<<"use \"list\" to get a list of valid column names\
n";
exit(1);
}
if (isDisplayed(cols, key)) {
mlog[WARN] <<"duplicate display request for \"" <<key <<"\" is i
gnored\n";
} else {
Column &c = appendColumn(cols, columnDecls[key]);
c.isDisplayed(true);
if (".a" == sorted) {
c.sorted(Sorted::ASCENDING);
} else if (".d" == sorted) {
c.sorted(Sorted::DESCENDING);
} else { } else {
gmtime_r(&t, &tm); ASSERT_require("" == sorted);
tz = "UTC";
} }
value = (boost::format("%04d-%02d-%02d %02d:%02d:%02d %s")
% (tm.tm_year + 1900) % (tm.tm_mon + 1) % tm.tm_mday
% tm.tm_hour % tm.tm_min % tm.tm_sec
% tz).str();
} else {
value = row.get<std::string>(j).orElse("null");
} }
} else {
mlog[FATAL] <<"unrecognized argument \"" <<StringUtility::cEscape(ar
g) <<"\"\n";
exit(1);
}
}
maybeConstrainRoseVersion(settings, db, cols, columnDecls);
// If we're deleting records, then the test ID must be one of the things we'
re selecting. But don't select it
// automatically--make the user do it so we know that they know what they're
doing.
if (settings.deleteMatchingTests) {
if (!isDisplayed(cols, "id")) {
mlog[FATAL] <<"the \"id\" field must be selected in order to delete
tests\n";
exit(1);
}
}
// If no columns are displayed, then select lots of them
if (nDisplayed(cols) == 0) {
displayMoreColumns(cols, columnDecls);
ASSERT_require(nDisplayed(cols) != 0);
}
// Make sure counts is selected (even it not displayed) so we can report how
many tests matched.
if (auto idx = find(cols, "count")) {
} else {
appendColumn(cols, columnDecls["count"]);
}
// Run the query
DB::Statement query = buildStatement(settings, db, cols);
// Generate the output per row of result
std::set<size_t> testIds;
FormattedTable table;
size_t nTests = 0, nRows = 0;
if (Format::YAML != settings.outputFormat)
buildTableColumnHeaders(table, cols);
for (auto row: query) {
if (++nRows > settings.limit.orElse(DEFAULT_ROW_LIMIT)) {
// We got more rows that the default limit. Don't print this last ro
w since it's just a guard to detect the
// overflow.
} else {
switch (settings.outputFormat) { switch (settings.outputFormat) {
case Format::PLAIN: case Format::PLAIN:
table.insert(i, j, value); case Format::HTML:
case Format::CSV:
case Format::SHELL:
nTests += emitTable(settings, table, row, cols, testIds /*ou
t*/);
break; break;
case Format::YAML: case Format::YAML:
std::cout <<(j?" ":"- ") <<columnsSelected[j] <<": " <<Stri ngUtility::yamlEscape(value) <<"\n"; nTests += emitYaml(settings, row, cols, testIds /*out*/);
break; break;
} }
} }
} }
switch (settings.outputFormat) {
case Format::PLAIN:
table.format(tableFormat(settings.outputFormat));
std::cout <<table
<<StringUtility::plural(nTests, "matching tests") <<"\n";
break;
case Format::CSV:
case Format::SHELL:
table.format(tableFormat(settings.outputFormat));
std::cout <<table;
SAWYER_MESG(mlog[INFO]) <<StringUtility::plural(nTests, "matching te
sts") <<"\n";
break;
case Format::HTML:
table.format(FormattedTable::Format::HTML);
std::cout <<table
<<"<p>" <<StringUtility::plural(nTests, "matching tests")
<<"</p>\n";
break;
case Format::YAML:
SAWYER_MESG(mlog[INFO]) <<StringUtility::plural(nTests, "matching te
sts") <<"\n";
break;
}
if (Format::PLAIN == settings.outputFormat) if (nRows > settings.limit.orElse(DEFAULT_ROW_LIMIT)) {
std::cout <<table; SAWYER_MESG(mlog[WARN]) <<"results were terminated after "
<<StringUtility::plural(settings.limit.orElse(DE
FAULT_ROW_LIMIT), "rows")
<<"; use --limit to see more\n";
}
// Delete tests
if (settings.deleteMatchingTests && !testIds.empty()) {
mlog[INFO] <<"deleting " <<StringUtility::plural(testIds.size(), "matchi
ng tests") <<"\n";
std::string inClause;
for (size_t id: testIds)
inClause += (inClause.empty() ? " in (" : ", ") + boost::lexical_cas
t<std::string>(id);
inClause += ")";
// Delete attachments first, then test records
db.stmt("delete from attachments where test_id " + inClause).run();
db.stmt("delete from test_results where id " + inClause).run();
}
} }
 End of changes. 45 change blocks. 
228 lines changed or deleted 1066 lines changed or added

Home  |  About  |  Features  |  All  |  Newest  |  Dox  |  Diffs  |  RSS Feeds  |  Screenshots  |  Comments  |  Imprint  |  Privacy  |  HTTP(S)