} | } | ||||
void dds::update_deps_info(database& db, const deps_info& deps) { | void dds::update_deps_info(database& db, const deps_info& deps) { | ||||
db.store_mtime(deps.output, fs::last_write_time(deps.output)); | |||||
db.store_file_command(deps.output, {deps.command, deps.command_output}); | db.store_file_command(deps.output, {deps.command, deps.command_output}); | ||||
db.forget_inputs_of(deps.output); | db.forget_inputs_of(deps.output); | ||||
for (auto&& inp : deps.inputs) { | for (auto&& inp : deps.inputs) { | ||||
db.store_mtime(inp, fs::last_write_time(inp)); | |||||
db.record_dep(inp, deps.output); | |||||
auto mtime = fs::last_write_time(inp); | |||||
db.record_dep(inp, deps.output, mtime); | |||||
} | } | ||||
} | } | ||||
auto& inputs = *inputs_; | auto& inputs = *inputs_; | ||||
auto changed_files = // | auto changed_files = // | ||||
inputs // | inputs // | ||||
| ranges::views::filter([](const seen_file_info& input) { | |||||
| ranges::views::filter([](const input_file_info& input) { | |||||
return !fs::exists(input.path) || fs::last_write_time(input.path) != input.last_mtime; | return !fs::exists(input.path) || fs::last_write_time(input.path) != input.last_mtime; | ||||
}) | }) | ||||
| ranges::views::transform([](auto& info) { return info.path; }) // | | ranges::views::transform([](auto& info) { return info.path; }) // |
} | } | ||||
// We must always generate deps info if it was possible: | // We must always generate deps info if it was possible: | ||||
assert(compiled_okay); | |||||
assert(ret_deps_info.has_value() || env.toolchain.deps_mode() == deps_mode::none); | assert(ret_deps_info.has_value() || env.toolchain.deps_mode() == deps_mode::none); | ||||
return ret_deps_info; | return ret_deps_info; | ||||
} | } |
db.exec(R"( | db.exec(R"( | ||||
CREATE TABLE dds_files ( | CREATE TABLE dds_files ( | ||||
file_id INTEGER PRIMARY KEY, | file_id INTEGER PRIMARY KEY, | ||||
path TEXT NOT NULL UNIQUE, | |||||
mtime INTEGER NOT NULL | |||||
path TEXT NOT NULL UNIQUE | |||||
); | |||||
CREATE TABLE dds_file_commands ( | |||||
command_id INTEGER PRIMARY KEY, | |||||
file_id | |||||
INTEGER | |||||
UNIQUE | |||||
NOT NULL | |||||
REFERENCES dds_files(file_id), | |||||
command TEXT NOT NULL, | |||||
output TEXT NOT NULL | |||||
); | ); | ||||
CREATE TABLE dds_deps ( | CREATE TABLE dds_deps ( | ||||
input_file_id | input_file_id | ||||
INTEGER | INTEGER | ||||
NOT NULL | NOT NULL | ||||
REFERENCES dds_files(file_id), | REFERENCES dds_files(file_id), | ||||
input_mtime INTEGER NOT NULL, | |||||
UNIQUE(input_file_id, output_file_id) | UNIQUE(input_file_id, output_file_id) | ||||
); | ); | ||||
CREATE TABLE dds_file_commands ( | |||||
command_id INTEGER PRIMARY KEY, | |||||
file_id | |||||
INTEGER | |||||
UNIQUE | |||||
NOT NULL | |||||
REFERENCES dds_files(file_id), | |||||
command TEXT NOT NULL, | |||||
output TEXT NOT NULL | |||||
); | |||||
)"); | )"); | ||||
} | } | ||||
database::database(sqlite3::database db) | database::database(sqlite3::database db) | ||||
: _db(std::move(db)) {} | : _db(std::move(db)) {} | ||||
std::optional<fs::file_time_type> database::last_mtime_of(path_ref file_) { | |||||
std::int64_t database::_record_file(path_ref path_) { | |||||
auto path = fs::weakly_canonical(path_); | |||||
sqlite3::exec(_stmt_cache(R"( | |||||
INSERT OR IGNORE INTO dds_files (path) | |||||
VALUES (?) | |||||
)"_sql), | |||||
std::forward_as_tuple(path.generic_string())); | |||||
auto& st = _stmt_cache(R"( | auto& st = _stmt_cache(R"( | ||||
SELECT mtime FROM dds_files WHERE path = ? | |||||
SELECT file_id | |||||
FROM dds_files | |||||
WHERE path = ?1 | |||||
)"_sql); | )"_sql); | ||||
st.reset(); | st.reset(); | ||||
auto path = fs::weakly_canonical(file_); | |||||
st.bindings[1] = path.string(); | |||||
auto maybe_res = sqlite3::unpack_single_opt<std::int64_t>(st); | |||||
if (!maybe_res) { | |||||
return std::nullopt; | |||||
} | |||||
auto [timestamp] = *maybe_res; | |||||
return fs::file_time_type(fs::file_time_type::duration(timestamp)); | |||||
auto str = path.generic_string(); | |||||
st.bindings[1] = str; | |||||
auto [rowid] = sqlite3::unpack_single<std::int64_t>(st); | |||||
return rowid; | |||||
} | } | ||||
void database::store_mtime(path_ref file, fs::file_time_type time) { | |||||
auto& st = _stmt_cache(R"( | |||||
INSERT INTO dds_files (path, mtime) | |||||
VALUES (?1, ?2) | |||||
ON CONFLICT(path) DO UPDATE SET mtime = ?2 | |||||
void database::record_dep(path_ref input, path_ref output, fs::file_time_type input_mtime) { | |||||
auto in_id = _record_file(input); | |||||
auto out_id = _record_file(output); | |||||
auto& st = _stmt_cache(R"( | |||||
INSERT OR IGNORE INTO dds_deps (input_file_id, output_file_id, input_mtime) | |||||
VALUES (?, ?, ?) | |||||
)"_sql); | )"_sql); | ||||
sqlite3::exec(st, | |||||
std::forward_as_tuple(fs::weakly_canonical(file).string(), | |||||
time.time_since_epoch().count())); | |||||
} | |||||
void database::record_dep(path_ref input, path_ref output) { | |||||
auto& st = _stmt_cache(R"( | |||||
WITH input AS ( | |||||
SELECT file_id | |||||
FROM dds_files | |||||
WHERE path = ?1 | |||||
), | |||||
output AS ( | |||||
SELECT file_id | |||||
FROM dds_files | |||||
WHERE path = ?2 | |||||
) | |||||
INSERT OR IGNORE INTO dds_deps (input_file_id, output_file_id) | |||||
VALUES ( | |||||
(SELECT * FROM input), | |||||
(SELECT * FROM output) | |||||
) | |||||
)"_sql); | |||||
sqlite3::exec(st, | |||||
std::forward_as_tuple(fs::weakly_canonical(input).string(), | |||||
fs::weakly_canonical(output).string())); | |||||
sqlite3::exec(st, std::forward_as_tuple(in_id, out_id, input_mtime.time_since_epoch().count())); | |||||
} | } | ||||
void database::store_file_command(path_ref file, const command_info& cmd) { | void database::store_file_command(path_ref file, const command_info& cmd) { | ||||
auto file_id = _record_file(file); | |||||
auto& st = _stmt_cache(R"( | auto& st = _stmt_cache(R"( | ||||
WITH file AS ( | |||||
SELECT file_id | |||||
FROM dds_files | |||||
WHERE path = ?1 | |||||
) | |||||
INSERT OR REPLACE | INSERT OR REPLACE | ||||
INTO dds_file_commands(file_id, command, output) | INTO dds_file_commands(file_id, command, output) | ||||
VALUES ( | |||||
(SELECT * FROM file), | |||||
?2, | |||||
?3 | |||||
) | |||||
VALUES (?1, ?2, ?3) | |||||
)"_sql); | )"_sql); | ||||
sqlite3::exec(st, | sqlite3::exec(st, | ||||
std::forward_as_tuple(fs::weakly_canonical(file).string(), | |||||
std::forward_as_tuple(file_id, | |||||
std::string_view(cmd.command), | std::string_view(cmd.command), | ||||
std::string_view(cmd.output))); | std::string_view(cmd.output))); | ||||
} | } | ||||
sqlite3::exec(st, std::forward_as_tuple(fs::weakly_canonical(file).string())); | sqlite3::exec(st, std::forward_as_tuple(fs::weakly_canonical(file).string())); | ||||
} | } | ||||
std::optional<std::vector<seen_file_info>> database::inputs_of(path_ref file_) { | |||||
std::optional<std::vector<input_file_info>> database::inputs_of(path_ref file_) { | |||||
auto file = fs::weakly_canonical(file_); | auto file = fs::weakly_canonical(file_); | ||||
auto& st = _stmt_cache(R"( | auto& st = _stmt_cache(R"( | ||||
WITH file AS ( | WITH file AS ( | ||||
SELECT file_id | SELECT file_id | ||||
FROM dds_files | FROM dds_files | ||||
WHERE path = ? | WHERE path = ? | ||||
), | |||||
input_ids AS ( | |||||
SELECT input_file_id | |||||
FROM dds_deps | |||||
WHERE output_file_id IN file | |||||
) | ) | ||||
SELECT path, mtime | |||||
FROM dds_files | |||||
WHERE file_id IN input_ids | |||||
SELECT path, input_mtime | |||||
FROM dds_deps | |||||
JOIN dds_files ON input_file_id = file_id | |||||
WHERE output_file_id IN file | |||||
)"_sql); | )"_sql); | ||||
st.reset(); | st.reset(); | ||||
st.bindings[1] = file.string(); | st.bindings[1] = file.string(); | ||||
auto tup_iter = sqlite3::iter_tuples<std::string, std::int64_t>(st); | auto tup_iter = sqlite3::iter_tuples<std::string, std::int64_t>(st); | ||||
std::vector<seen_file_info> ret; | |||||
std::vector<input_file_info> ret; | |||||
for (auto& [path, mtime] : tup_iter) { | for (auto& [path, mtime] : tup_iter) { | ||||
ret.emplace_back( | ret.emplace_back( | ||||
seen_file_info{path, fs::file_time_type(fs::file_time_type::duration(mtime))}); | |||||
input_file_info{path, fs::file_time_type(fs::file_time_type::duration(mtime))}); | |||||
} | } | ||||
if (ret.empty()) { | if (ret.empty()) { |
std::string output; | std::string output; | ||||
}; | }; | ||||
struct seen_file_info { | |||||
struct input_file_info { | |||||
fs::path path; | fs::path path; | ||||
fs::file_time_type last_mtime; | fs::file_time_type last_mtime; | ||||
}; | }; | ||||
explicit database(neo::sqlite3::database db); | explicit database(neo::sqlite3::database db); | ||||
database(const database&) = delete; | database(const database&) = delete; | ||||
std::int64_t _record_file(path_ref p); | |||||
public: | public: | ||||
static database open(const std::string& db_path); | static database open(const std::string& db_path); | ||||
static database open(path_ref db_path) { return open(db_path.string()); } | static database open(path_ref db_path) { return open(db_path.string()); } | ||||
return neo::sqlite3::transaction_guard(_db); | return neo::sqlite3::transaction_guard(_db); | ||||
} | } | ||||
std::optional<fs::file_time_type> last_mtime_of(path_ref file); | |||||
void store_mtime(path_ref file, fs::file_time_type time); | |||||
void record_dep(path_ref input, path_ref output); | |||||
void store_file_command(path_ref file, const command_info& cmd); | |||||
void forget_inputs_of(path_ref file); | |||||
void record_dep(path_ref input, path_ref output, fs::file_time_type input_mtime); | |||||
void store_file_command(path_ref file, const command_info& cmd); | |||||
void forget_inputs_of(path_ref file); | |||||
std::optional<std::vector<seen_file_info>> inputs_of(path_ref file); | |||||
std::optional<std::vector<input_file_info>> inputs_of(path_ref file); | |||||
std::optional<command_info> command_of(path_ref file); | std::optional<command_info> command_of(path_ref file); | ||||
}; | }; | ||||
using namespace std::literals; | using namespace std::literals; | ||||
TEST_CASE("Create a database") { auto db = dds::database::open(":memory:"s); } | TEST_CASE("Create a database") { auto db = dds::database::open(":memory:"s); } | ||||
TEST_CASE("Read an absent file's mtime") { | |||||
auto db = dds::database::open(":memory:"s); | |||||
auto mtime_opt = db.last_mtime_of("bad/file/path"); | |||||
CHECK_FALSE(mtime_opt.has_value()); | |||||
} | |||||
TEST_CASE("Record a file") { | |||||
auto db = dds::database::open(":memory:"s); | |||||
auto time = dds::fs::file_time_type::clock::now(); | |||||
db.store_mtime("file/something", time); | |||||
auto mtime_opt = db.last_mtime_of("file/something"); | |||||
REQUIRE(mtime_opt.has_value()); | |||||
CHECK(mtime_opt == time); | |||||
} |
std::string tc_content; | std::string tc_content; | ||||
if (starts_with(tc_id, "debug:")) { | |||||
tc_id = tc_id.substr("debug:"sv.length()); | |||||
tc_content += "Debug: True\n"; | |||||
} | |||||
if (starts_with(tc_id, "ccache:")) { | if (starts_with(tc_id, "ccache:")) { | ||||
tc_id = tc_id.substr("ccache:"sv.length()); | tc_id = tc_id.substr("ccache:"sv.length()); | ||||
tc_content += "Compiler-Launcher: ccache\n"; | tc_content += "Compiler-Launcher: ccache\n"; |
#include "./values.hpp" | |||||
int value_1() { return first_value; } |
#include "./values.hpp" | |||||
int value_2() { return second_value; } |
#include "./foo.hpp" | |||||
#include <cmath> | |||||
int main() { return std::abs(value_1() - value_2()); } |
#pragma once | |||||
int value_1(); | |||||
int value_2(); |
#pragma once | |||||
const int first_value = 32; | |||||
const int second_value = 32; |
import subprocess | |||||
import time | |||||
import pytest | |||||
from tests import dds, DDS, dds_fixture_conf_1 | |||||
from dds_ci import proc | |||||
## ############################################################################# | |||||
## ############################################################################# | |||||
## The test project in this directory contains a single application and two | |||||
## functions, each defined in a separate file. The two functions each return | |||||
## an integer, and the application exit code will be the difference between | |||||
## the two integers. (They are passed through std::abs(), so it is always a | |||||
## positive integer). The default value is 32 in both functions. | |||||
## ############################################################################# | |||||
## The purpose of these tests is to ensure the reliability of the compilation | |||||
## dependency database. Having a miscompile because there was a failure to | |||||
## detect file changes is a catastrophic bug! | |||||
def build_and_get_rc(dds: DDS) -> int: | |||||
dds.build() | |||||
app = dds.build_dir / ('app' + dds.exe_suffix) | |||||
return proc.run(app).returncode | |||||
def test_simple_rebuild(dds: DDS): | |||||
""" | |||||
Check that changing a source file will update the resulting application. | |||||
""" | |||||
assert build_and_get_rc(dds) == 0 | |||||
dds.scope.enter_context( | |||||
dds.set_contents( | |||||
'src/1.cpp', | |||||
b''' | |||||
int value_1() { return 33; } | |||||
''', | |||||
)) | |||||
# 33 - 32 = 1 | |||||
assert build_and_get_rc(dds) == 1 | |||||
def test_rebuild_header_change(dds: DDS): | |||||
"""Change the content of the header which defines the values""" | |||||
assert build_and_get_rc(dds) == 0 | |||||
dds.scope.enter_context( | |||||
dds.set_contents( | |||||
'src/values.hpp', | |||||
b''' | |||||
const int first_value = 63; | |||||
const int second_value = 88; | |||||
''', | |||||
)) | |||||
assert build_and_get_rc(dds) == (88 - 63) | |||||
def test_partial_build_rebuild(dds: DDS): | |||||
""" | |||||
Change the content of a header, but cause one user of that header to fail | |||||
compilation. The fact that compilation fails means it is still `out-of-date`, | |||||
and will need to be compiled after we have fixed it up. | |||||
""" | |||||
assert build_and_get_rc(dds) == 0 | |||||
dds.scope.enter_context( | |||||
dds.set_contents( | |||||
'src/values.hpp', | |||||
b''' | |||||
const int first_value_q = 6; | |||||
const int second_value_q = 99; | |||||
''', | |||||
)) | |||||
# Header now causes errors in 1.cpp and 2.cpp | |||||
with pytest.raises(subprocess.CalledProcessError): | |||||
dds.build() | |||||
# Fix 1.cpp | |||||
dds.scope.enter_context( | |||||
dds.set_contents( | |||||
'src/1.cpp', | |||||
b''' | |||||
#include "./values.hpp" | |||||
int value_1() { return first_value_q; } | |||||
''', | |||||
)) | |||||
# We will still see a failure, but now the DB will record the updated values.hpp | |||||
with pytest.raises(subprocess.CalledProcessError): | |||||
dds.build() | |||||
# Should should raise _again_, even though we've successfully compiled one | |||||
# of the two files with the changed `values.hpp`, because `2.cpp` still | |||||
# has a pending update | |||||
with pytest.raises(subprocess.CalledProcessError): | |||||
dds.build() | |||||
dds.scope.enter_context( | |||||
dds.set_contents( | |||||
'src/2.cpp', | |||||
b''' | |||||
#include "./values.hpp" | |||||
int value_2() { return second_value_q; } | |||||
''', | |||||
)) | |||||
# We should now compile and link to get the updated value | |||||
assert build_and_get_rc(dds) == (99 - 6) |