DB.pm 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. package Test::Mapper::DB;
  2. use strict;
  3. use warnings;
  4. use DBI;
  5. use File::Touch;
  6. use File::Slurper qw{read_text};
  7. # ABSTRACT: Persistent storage for what test maps where for what build.
  8. # We hold possibly multiple DBHes to support testing multiple products.
  9. my $dbh = {};
  10. sub dbh {
  11. my ( $dbname ) = @_;
  12. return $dbh->{$dbname} if $dbh->{$dbname};
  13. File::Touch::touch($dbname) unless -f $dbname;
  14. my $db = DBI->connect( "dbi:SQLite:dbname=$dbname", "", "" );
  15. # Turn on fkeys before schema creation so FK constraints take effect
  16. $db->do("PRAGMA foreign_keys = ON") or die "Could not enable foreign keys";
  17. # Turn on WAL mode for concurrent reads
  18. $db->do("PRAGMA journal_mode = WAL") or die "Could not enable WAL mode";
  19. my $schema = join( "", readline(DATA) );
  20. if ($schema) {
  21. $db->{sqlite_allow_multiple_statements} = 1;
  22. $db->do($schema) or die "Could not ensure database consistency: " . $db->errstr;
  23. $db->{sqlite_allow_multiple_statements} = 0;
  24. }
  25. $dbh->{$dbname} = $db;
  26. return $db;
  27. }
  28. 1;
  29. __DATA__
  30. -- Should be pretty self-explanatory, DRY
  31. CREATE TABLE IF NOT EXISTS product (
  32. id INTEGER PRIMARY KEY AUTOINCREMENT,
  33. product TEXT NOT NULL UNIQUE
  34. );
  35. CREATE TABLE IF NOT EXISTS environment (
  36. id INTEGER PRIMARY KEY AUTOINCREMENT,
  37. environment TEXT NOT NULL,
  38. product_id INTEGER NOT NULL REFERENCES product(id) ON DELETE CASCADE,
  39. UNIQUE(environment, product_id) ON CONFLICT IGNORE
  40. );
  41. CREATE TABLE IF NOT EXISTS build (
  42. id INTEGER PRIMARY KEY AUTOINCREMENT,
  43. build TEXT NOT NULL,
  44. product_id INTEGER NOT NULL REFERENCES product(id) ON DELETE CASCADE,
  45. UNIQUE(build, product_id) ON CONFLICT IGNORE
  46. );
  47. CREATE TABLE IF NOT EXISTS route (
  48. id INTEGER PRIMARY KEY AUTOINCREMENT,
  49. route TEXT NOT NULL,
  50. product_id INTEGER NOT NULL REFERENCES product(id) ON DELETE CASCADE,
  51. UNIQUE(route, product_id) ON CONFLICT IGNORE
  52. );
  53. CREATE TABLE IF NOT EXISTS test (
  54. id INTEGER PRIMARY KEY AUTOINCREMENT,
  55. filename TEXT NOT NULL,
  56. product_id INTEGER NOT NULL REFERENCES product(id) ON DELETE CASCADE,
  57. UNIQUE(filename, product_id) ON CONFLICT IGNORE
  58. );
  59. -- run_by is used to coordinate running, what hasn't been taken yet etc
  60. -- result is a result code that visualization programs can use for historical charts
  61. -- The idea here is that 'people just wanna see what failed', and this helps narrow that down
  62. -- start/finish is so you can figure historical data/charts
  63. CREATE TABLE IF NOT EXISTS test_for_build (
  64. id INTEGER PRIMARY KEY AUTOINCREMENT,
  65. build_id INTEGER NOT NULL REFERENCES build(id) ON DELETE CASCADE,
  66. test_id INTEGER NOT NULL REFERENCES test(id) ON DELETE CASCADE,
  67. run_by TEXT,
  68. result INTEGER,
  69. start INTEGER NOT NULL DEFAULT CURRENT_TIMESTAMP,
  70. finish INTEGER DEFAULT NULL,
  71. UNIQUE(build_id, test_id) ON CONFLICT ROLLBACK
  72. );
  73. -- Actually do the mapping of test -> what files changing trigger its running
  74. -- One tends to use differing resources on different environments.
  75. -- It is however assumed that the tests (or the cron inserting resources/tests) discriminates in this regard.
  76. CREATE TABLE IF NOT EXISTS resource (
  77. id INTEGER PRIMARY KEY AUTOINCREMENT,
  78. environment_id INTEGER NOT NULL REFERENCES environment(id) ON DELETE CASCADE,
  79. filename TEXT NOT NULL,
  80. UNIQUE(environment_id, filename) ON CONFLICT IGNORE
  81. );
  82. -- Indexed by resource_id, as that's how a build is done "I have x changed resources, wat run?"
  83. CREATE TABLE IF NOT EXISTS tested_by (
  84. id INTEGER PRIMARY KEY AUTOINCREMENT,
  85. resource_id INTEGER NOT NULL REFERENCES resource(id) ON DELETE CASCADE,
  86. test_id INTEGER NOT NULL REFERENCES test(id) ON DELETE CASCADE,
  87. UNIQUE(resource_id, test_id) ON CONFLICT IGNORE
  88. );
  89. -- Lifecycle:
  90. -- Have list of files from `git log`
  91. -- Run analysis script that returns hash test_file => [source_file, ...]
  92. -- INSERT OR IGNORE INTO tested_by (resource_id, test_id) VALUES (...);
  93. CREATE VIEW IF NOT EXISTS to_run AS
  94. SELECT
  95. t.filename AS test_file,
  96. t.id AS test_id,
  97. r.filename AS source_file,
  98. e.environment AS environment
  99. FROM resource AS r
  100. JOIN tested_by AS tb ON tb.resource_id = r.id
  101. JOIN test AS t ON t.id = tb.test_id
  102. JOIN environment AS e ON e.id = r.environment_id
  103. ;
  104. -- Lifecycle:
  105. -- Cron dumps rows into resource/tested_by, when time comes to run we grab the list of stuff:
  106. -- SELECT test_file FROM to_run WHERE source_file IN (...) GROUP BY test_file;
  107. -- We make a run
  108. -- INSERT OR IGNORE INTO test_for_build (build_id, test_id) SELECT b.id, tr.test_id
  109. -- FROM build AS b, to_run AS tr WHERE b.build='someSha' AND tr.source_file IN (...);
  110. -- We run a test
  111. -- UPDATE test_for_build SET run_by='processid:90210' WHERE build_id=? AND test_id=?;
  112. -- It completes. The end.
  113. -- UPDATE test_for_build SET result=1, finish=CURRENT_TIMESTAMP WHERE build_id=? AND test_id=?;
  114. CREATE VIEW IF NOT EXISTS run AS
  115. SELECT
  116. p.product,
  117. e.environment,
  118. b.build,
  119. r.route,
  120. t.filename,
  121. tb.run_by,
  122. tb.result
  123. FROM product AS p
  124. JOIN build AS b ON p.id = b.product_id
  125. JOIN test_for_build AS tb ON b.id = tb.build_id
  126. JOIN environment AS e ON p.id = e.product_id
  127. JOIN route AS r ON p.id = r.product_id
  128. JOIN test AS t ON tb.test_id = t.id
  129. ;