diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 6ce3ab4..ed4483c 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -48,4 +48,7 @@ if [[ ! -S "$socket_path" ]]; then fi url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${include_kill}&skip_local_service_pages=${skip_local_service_pages}" -exec curl -sS --fail-with-body --unix-socket "$socket_path" "$url" +curl -sS --fail-with-body --unix-socket "$socket_path" "$url" +if [[ "$action" == "run" ]]; then + scripts/test_dependency_invalidation.sh +fi diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh new file mode 100755 index 0000000..d592443 --- /dev/null +++ b/scripts/test_dependency_invalidation.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="dependency-cache-test-$$" +source_dir="site/$test_name" +cache_dir="${BIN_DIRECTORY:-/tmp/uce/work}$(pwd)/$source_dir" + +cleanup() { + rm -rf "$source_dir" "$cache_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir" + +printf '%s\n' \ + '#ifndef UCE_DEPENDENCY_CACHE_CHILD' \ + '#define UCE_DEPENDENCY_CACHE_CHILD' \ + 'String dependency_cache_marker() { return("dependency-marker-a"); }' \ + '#endif' >"$source_dir/child.uce" +printf '%s\n' \ + '#load "child.uce"' \ + 'CLI(Request& context) { print(dependency_cache_marker()); }' \ + 'RENDER(Request& context) { print(dependency_cache_marker()); }' >"$source_dir/parent.uce" + +first=$(scripts/uce-cli "/$test_name/parent.uce") +if [[ "$first" != *dependency-marker-a* ]]; then + echo "dependency invalidation setup failed: $first" >&2 + exit 1 +fi + +sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce" +second=$(scripts/uce-cli "/$test_name/parent.uce") +if [[ "$second" != *dependency-marker-b* ]]; then + echo "loaded dependency change did not recompile parent: $second" >&2 + exit 1 +fi + +echo "dependency invalidation passed" diff --git a/site/doc/pages/load.txt b/site/doc/pages/load.txt index ec11054..cc1ca3a 100644 --- a/site/doc/pages/load.txt +++ b/site/doc/pages/load.txt @@ -12,6 +12,8 @@ Includes another UCE file. Use `#load` when you want the current file to pull in declarations or reusable content from another UCE source file. +Loaded sources are tracked transitively. Changing a direct or nested `#load` dependency invalidates and recompiles the entry unit before its next execution. + :example // #load "file.uce" includes another unit at COMPILE time (a preprocessor directive, diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index a0fb5c5..c2934f1 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -87,6 +87,55 @@ StringMap compiler_parse_unit_metadata(String content) return(metadata); } +StringList compiler_unit_load_paths(String file_name, String content) +{ + StringList paths; + u64 line_start = 0; + while(line_start < content.length()) + { + u64 line_end = content.find("\n", line_start); + if(line_end == String::npos) + break; + String line = content.substr(line_start, line_end - line_start); + if(str_starts_with(line, "#load ")) + { + u64 first_quote = line.find('"', 6); + u64 second_quote = first_quote == String::npos ? String::npos : line.find('"', first_quote + 1); + if(first_quote != String::npos && second_quote != String::npos) + { + String loaded = line.substr(first_quote + 1, second_quote - first_quote - 1); + if(loaded != "") + paths.push_back(loaded[0] == '/' ? loaded : expand_path(loaded, dirname(file_name))); + } + } + line_start = line_end + 1; + } + return(paths); +} + +void compiler_append_unit_source_signature(String file_name, std::set& visited, String& signature) +{ + String normalized = path_real(file_name); + if(normalized == "") + normalized = file_name; + if(visited.find(normalized) != visited.end()) + return; + visited.insert(normalized); + + String content = file_get_contents(file_name); + signature += normalized + ":" + gen_sha1(content) + "\n"; + for(String loaded : compiler_unit_load_paths(file_name, content)) + compiler_append_unit_source_signature(loaded, visited, signature); +} + +String compiler_unit_source_signature(String file_name) +{ + std::set visited; + String signature = "uce-load-graph-v1\n"; + compiler_append_unit_source_signature(file_name, visited, signature); + return(gen_sha1(signature)); +} + String compiler_unit_input_signature(Request* context, SharedUnit* su) { if(!context || !su || !file_exists(su->file_name)) @@ -94,7 +143,7 @@ String compiler_unit_input_signature(Request* context, SharedUnit* su) String setup_template = context->server->config["COMPILER_SYS_PATH"] + "/" + context->server->config["SETUP_TEMPLATE"]; return( - gen_sha1(file_get_contents(su->file_name)) + ":" + + compiler_unit_source_signature(su->file_name) + ":" + gen_sha1(file_get_contents(setup_template)) + ":" + std::to_string(UCE_UNIT_ABI_VERSION) );