mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
fix(make): a node per rule target; target-variable lines are no rules
Make rules were read with three gaps: - A rule naming several targets (`seed-up seed-dev seed-down: ...`, `kind-% seed-%:`) became one Function named after its first target: `seed-down` had no node, so nothing could link `make seed-down` and a search for it found nothing. Every target now gets a Function over the rule's span (without fingerprint, structural profile or body tokens, so the copies of one recipe do not read as similar code), and the recipe's DEPLOYS / SPAWNS edges leave from each of them. - A target-specific variable line (`kind-%: export X := y`) was taken for a rule: a phantom Function for its first target, or a variant span merged into the real rule of that name. Such a line is no rule now. Its tree-sitter ERROR region is no longer masked, so the file reports it as partially parsed, as for any other unparsed region. - `include hack/tools.mk` imported a Folder `hack/tools` (local name "mk\n\n": the generic import reader kept the directive's newlines and split at the extension). Make includes are read on their own now: every name of include / -include / sinclude, outside recipes, imports that file; a name with a variable reference is left out. Measured on 17 repositories against an independent reading: make runs linked 298 of 300 (was 282), all 298 correct; the 2 left pick their target in an if/else, which stays unknown by design. Graph changes are confined to Makefiles (gardener +26 targets, 13 phantoms removed; deckhouse +4), their edges, and two side effects of new nodes: SEMANTICALLY_RELATED edges elsewhere re-rank, and one Go member reference in deckhouse lost its unique-name binding to a new Make rule of the same name. Three runs build identical graphs; index time unchanged (deckhouse 9.61 -> 9.60 s, gardener 5.24 -> 5.27 s, cluster-api 2.68 -> 2.68 s, medians); renaming a target, editing a Makefile or an included file leaves the graph a fresh index builds (gardener's Makefile edit differed before, through the phantom nodes, and no longer does). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
1 parent
b080e982de
commit
7842cd8a69
4 files changed
+294
-20
No files matched your search
@@ -6275,6 +6275,107 @@ void cbm_test_declarations_finish(CBMExtractCtx *ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
static bool make_name_char(char c) {
|
||||
return c == '_' || c == '.' || c == '-' || (c >= '0' && c <= '9') ||
|
||||
((c | ' ') >= 'a' && (c | ' ') <= 'z');
|
||||
}
|
||||
|
||||
/* A `rule` node that is a target-specific variable: after the targets'
|
||||
* colon(s) come [export|override|private|unexport]... NAME and an
|
||||
* assignment operator (= := ::= :::= += ?= !=). */
|
||||
static bool make_rule_sets_variable(TSNode rule, const char *src) {
|
||||
static const char *const kws[] = {"export", "override", "private", "unexport", NULL};
|
||||
TSNode targets = cbm_find_child_by_kind(rule, "targets");
|
||||
if (ts_node_is_null(targets)) {
|
||||
return false;
|
||||
}
|
||||
uint32_t p = ts_node_end_byte(targets);
|
||||
uint32_t end = ts_node_end_byte(rule);
|
||||
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
|
||||
p++;
|
||||
}
|
||||
if (p >= end || src[p] != ':') {
|
||||
return false;
|
||||
}
|
||||
while (p < end && src[p] == ':') {
|
||||
p++;
|
||||
}
|
||||
for (bool kw = true; kw;) {
|
||||
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
|
||||
p++;
|
||||
}
|
||||
kw = false;
|
||||
for (int i = 0; kws[i]; i++) {
|
||||
size_t n = strlen(kws[i]);
|
||||
if (p + n < end && strncmp(src + p, kws[i], n) == 0 &&
|
||||
(src[p + n] == ' ' || src[p + n] == '\t')) {
|
||||
p += (uint32_t)n;
|
||||
kw = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
uint32_t name = p;
|
||||
while (p < end && make_name_char(src[p])) {
|
||||
p++;
|
||||
}
|
||||
if (p == name) {
|
||||
return false;
|
||||
}
|
||||
while (p < end && (src[p] == ' ' || src[p] == '\t')) {
|
||||
p++;
|
||||
}
|
||||
if (p < end && src[p] == ':') {
|
||||
while (p < end && src[p] == ':') {
|
||||
p++;
|
||||
}
|
||||
return p < end && src[p] == '=';
|
||||
}
|
||||
return p < end && (src[p] == '=' ||
|
||||
(strchr("+?!", src[p]) && p + SKIP_ONE < end && src[p + SKIP_ONE] == '='));
|
||||
}
|
||||
|
||||
/* Make: a rule names several targets (`up down: deps`), each a goal
|
||||
* `make <target>` runs. The rule's definition carries the first target;
|
||||
* every further one gets a definition of its own over the same rule span --
|
||||
* without fingerprint, structural profile or body tokens, so the copies of
|
||||
* one recipe do not read as similar code. Special (`.X`) targets and
|
||||
* repeats stay out. */
|
||||
static void push_make_rule_targets(CBMExtractCtx *ctx, TSNode node, const CBMDefinition *first) {
|
||||
CBMArena *a = ctx->arena;
|
||||
TSNode targets = cbm_find_child_by_kind(node, "targets");
|
||||
uint32_t n = ts_node_is_null(targets) ? 0 : ts_node_named_child_count(targets);
|
||||
for (uint32_t i = SKIP_ONE; i < n; i++) {
|
||||
char *name =
|
||||
cbm_func_name_node_text(a, ts_node_named_child(targets, i), ctx->source, ctx->language);
|
||||
if (!name || !name[0] || name[0] == '.' || strcmp(name, first->name) == 0) {
|
||||
continue;
|
||||
}
|
||||
bool repeat = false;
|
||||
for (uint32_t j = SKIP_ONE; j < i && !repeat; j++) {
|
||||
char *prev = cbm_func_name_node_text(a, ts_node_named_child(targets, j), ctx->source,
|
||||
ctx->language);
|
||||
repeat = prev && strcmp(prev, name) == 0;
|
||||
}
|
||||
if (repeat) {
|
||||
continue;
|
||||
}
|
||||
CBMDefinition d = *first;
|
||||
d.name = name;
|
||||
d.qualified_name =
|
||||
cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path, name, ctx->language);
|
||||
d.is_exported = cbm_is_exported(name, ctx->language);
|
||||
d.is_entry_point = strcmp(name, "main") == 0;
|
||||
d.fingerprint = NULL;
|
||||
d.fingerprint_k = 0;
|
||||
d.structural_profile = NULL;
|
||||
d.body_tokens = NULL;
|
||||
if (d.qualified_name) {
|
||||
cbm_defs_push(&ctx->result->defs, a, d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
|
||||
CBMArena *a = ctx->arena;
|
||||
|
||||
@@ -6308,6 +6409,12 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
if (ctx->language == CBM_LANG_MAKEFILE && name[0] == '.') {
|
||||
return;
|
||||
}
|
||||
// `targets: [export] VAR := value` sets a variable for those targets:
|
||||
// no rule, so no def (else a target only ever configured there is a
|
||||
// phantom, and a real rule of it gains the line as a variant).
|
||||
if (ctx->language == CBM_LANG_MAKEFILE && make_rule_sets_variable(node, ctx->source)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* C++/CUDA: GoogleTest macros (TEST, TEST_F, TEST_P, ...) parse as
|
||||
* function definitions whose name resolves to the bare macro identifier.
|
||||
@@ -6557,6 +6664,9 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
|
||||
return;
|
||||
}
|
||||
cbm_defs_push(&ctx->result->defs, a, def);
|
||||
if (ctx->language == CBM_LANG_MAKEFILE && strcmp(ts_node_type(node), "rule") == 0) {
|
||||
push_make_rule_targets(ctx, node, &def);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Class definition extraction ---
|
||||
|
||||
@@ -48,6 +48,7 @@ static void parse_starlark_imports(CBMExtractCtx *ctx);
|
||||
static void parse_tcl_imports(CBMExtractCtx *ctx);
|
||||
static void parse_teal_imports(CBMExtractCtx *ctx);
|
||||
static void parse_shell_source_imports(CBMExtractCtx *ctx);
|
||||
static void parse_make_imports(CBMExtractCtx *ctx);
|
||||
static void parse_css_imports(CBMExtractCtx *ctx);
|
||||
static void parse_html_imports(CBMExtractCtx *ctx);
|
||||
static void parse_cmake_imports(CBMExtractCtx *ctx);
|
||||
@@ -2221,6 +2222,52 @@ static void parse_shell_source_imports(CBMExtractCtx *ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- Make imports: include / -include / sinclude FILE... ---
|
||||
// Every name of an include directive -- outside recipes, conditionals
|
||||
// included -- imports that file; its local name is the file's base name. A
|
||||
// name holding a variable reference is known only to make and stays out.
|
||||
// (The generic text fallback kept the directive's trailing newlines in the
|
||||
// path and split it at the extension's dot.)
|
||||
static void push_make_include(CBMExtractCtx *ctx, TSNode word) {
|
||||
CBMArena *a = ctx->arena;
|
||||
char *path = cbm_node_text(a, word, ctx->source);
|
||||
if (!path || !path[0] || strchr(path, '$')) {
|
||||
return;
|
||||
}
|
||||
const char *slash = strrchr(path, '/');
|
||||
CBMImport imp = {.local_name = slash ? slash + SKIP_ONE : path, .module_path = path};
|
||||
cbm_imports_push(&ctx->result->imports, a, imp);
|
||||
}
|
||||
|
||||
static void parse_make_imports(CBMExtractCtx *ctx) {
|
||||
TSNodeStack stack;
|
||||
ts_nstack_init(&stack, ctx, CBM_SZ_512);
|
||||
ts_nstack_push(&stack, ctx->root);
|
||||
while (stack.count > 0) {
|
||||
TSNode node = ts_nstack_pop(&stack);
|
||||
const char *k = ts_node_type(node);
|
||||
if (strcmp(k, "recipe") == 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(k, "include_directive") != 0) {
|
||||
ts_nstack_push_children(&stack, node);
|
||||
continue;
|
||||
}
|
||||
TSNode names = ts_node_child_by_field_name(node, TS_FIELD("filenames"));
|
||||
if (ts_node_is_null(names)) {
|
||||
continue;
|
||||
}
|
||||
uint32_t n = ts_node_named_child_count(names);
|
||||
if (strcmp(ts_node_type(names), "list") != 0 || n == 0) {
|
||||
push_make_include(ctx, names);
|
||||
continue;
|
||||
}
|
||||
for (uint32_t i = 0; i < n; i++) {
|
||||
push_make_include(ctx, ts_node_named_child(names, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- CSS / SCSS imports ---
|
||||
// CSS @import "x.css" and SCSS @use/@forward 'x' are top-level statements that
|
||||
// carry a `string_value` (whose `string_content` is the unquoted path). The
|
||||
@@ -3054,6 +3101,9 @@ void cbm_extract_imports(CBMExtractCtx *ctx) {
|
||||
case CBM_LANG_BASH:
|
||||
parse_shell_source_imports(ctx);
|
||||
break;
|
||||
case CBM_LANG_MAKEFILE:
|
||||
parse_make_imports(ctx);
|
||||
break;
|
||||
case CBM_LANG_ZIG:
|
||||
parse_zig_imports(ctx);
|
||||
break;
|
||||
|
||||
+55
-20
@@ -3138,10 +3138,25 @@ static int sh_cmp_span(const void *a, const void *b) {
|
||||
return strcmp(x->file, y->file);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const cbm_gbuf_node_t **items;
|
||||
int count;
|
||||
int cap;
|
||||
} sh_srcs_t;
|
||||
|
||||
static int sh_cmp_node_qn(const void *a, const void *b) {
|
||||
const cbm_gbuf_node_t *x = *(const cbm_gbuf_node_t *const *)a;
|
||||
const cbm_gbuf_node_t *y = *(const cbm_gbuf_node_t *const *)b;
|
||||
return strcmp(x->qualified_name ? x->qualified_name : "",
|
||||
y->qualified_name ? y->qualified_name : "");
|
||||
}
|
||||
|
||||
/* The innermost Function around `line` in `file`, else its Module, else its
|
||||
* File node. */
|
||||
* File node. Into `twins` (when given): every Function of exactly that
|
||||
* span, in QN order -- the targets of one Make rule share it, and the
|
||||
* rule's recipe runs for each of them. */
|
||||
static int64_t sh_source(cbm_pipeline_ctx_t *ctx, const sh_span_t *spans, int n,
|
||||
CBMHashTable *modules, const char *file, int line) {
|
||||
CBMHashTable *modules, const char *file, int line, sh_srcs_t *twins) {
|
||||
int lo = 0;
|
||||
int hi = n;
|
||||
while (lo < hi) {
|
||||
@@ -3167,6 +3182,17 @@ static int64_t sh_source(cbm_pipeline_ctx_t *ctx, const sh_span_t *spans, int n,
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
for (int i = lo; twins && i < n && strcmp(spans[i].file, file) == 0; i++) {
|
||||
const cbm_gbuf_node_t *f = spans[i].node;
|
||||
if (f->start_line == best->start_line && f->end_line == best->end_line &&
|
||||
sh_grow((void **)&twins->items, &twins->cap, twins->count, sizeof(*twins->items))) {
|
||||
twins->items[twins->count++] = f;
|
||||
}
|
||||
}
|
||||
if (twins && twins->count > SKIP_ONE) {
|
||||
qsort((void *)twins->items, (size_t)twins->count, sizeof(*twins->items),
|
||||
sh_cmp_node_qn);
|
||||
}
|
||||
return best->id;
|
||||
}
|
||||
int64_t mod = (int64_t)(intptr_t)cbm_ht_get(modules, file);
|
||||
@@ -3247,6 +3273,27 @@ static int sh_link_spawn(cbm_pipeline_ctx_t *ctx, int64_t src, const sh_hit_t *x
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
/* The edge hit `x` draws from source `src`; 1 when drawn. */
|
||||
static int sh_link_hit(cbm_pipeline_ctx_t *ctx, const sh_hit_t *x, int64_t src,
|
||||
CBMHashTable *modules) {
|
||||
if (x->spawn) {
|
||||
return src > 0 ? sh_link_spawn(ctx, src, x, modules) : 0;
|
||||
}
|
||||
int64_t tgt = x->tgt > 0 ? x->tgt : sh_external_chart(ctx, x->ext, x->file);
|
||||
if (src <= 0 || tgt <= 0 || src == tgt) {
|
||||
return 0;
|
||||
}
|
||||
char esc[CBM_SZ_1K];
|
||||
char ci[CBM_SZ_1K];
|
||||
char props[CBM_SZ_4K];
|
||||
cbm_json_escape(esc, (int)sizeof(esc), x->path ? x->path : "");
|
||||
sh_ci_props(x, ci, sizeof(ci));
|
||||
snprintf(props, sizeof(props), "{\"via\":\"%s\",\"%s\":\"%s\"%s%s,\"line\":%d}", x->via,
|
||||
x->chart ? "chart" : "path", esc, x->tgt > 0 ? "" : ",\"external\":true", ci, x->line);
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src, tgt, "DEPLOYS", props);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
|
||||
static int sh_link(cbm_pipeline_ctx_t *ctx, const sh_hits_t *h) {
|
||||
CBMHashTable *files = cbm_ht_create(0);
|
||||
CBMHashTable *modules = cbm_ht_create(0);
|
||||
@@ -3293,28 +3340,16 @@ static int sh_link(cbm_pipeline_ctx_t *ctx, const sh_hits_t *h) {
|
||||
}
|
||||
}
|
||||
int edges = 0;
|
||||
sh_srcs_t twins = {0};
|
||||
for (int i = 0; i < h->count; i++) {
|
||||
const sh_hit_t *x = &h->items[i];
|
||||
int64_t src = sh_source(ctx, spans, n, modules, x->file, x->line);
|
||||
if (x->spawn) {
|
||||
edges += src > 0 ? sh_link_spawn(ctx, src, x, modules) : 0;
|
||||
continue;
|
||||
twins.count = 0;
|
||||
int64_t one = sh_source(ctx, spans, n, modules, x->file, x->line, &twins);
|
||||
for (int k = 0; k < (twins.count ? twins.count : SKIP_ONE); k++) {
|
||||
edges += sh_link_hit(ctx, x, twins.count ? twins.items[k]->id : one, modules);
|
||||
}
|
||||
int64_t tgt = x->tgt > 0 ? x->tgt : sh_external_chart(ctx, x->ext, x->file);
|
||||
if (src <= 0 || tgt <= 0 || src == tgt) {
|
||||
continue;
|
||||
}
|
||||
char esc[CBM_SZ_1K];
|
||||
char ci[CBM_SZ_1K];
|
||||
char props[CBM_SZ_4K];
|
||||
cbm_json_escape(esc, (int)sizeof(esc), x->path ? x->path : "");
|
||||
sh_ci_props(x, ci, sizeof(ci));
|
||||
snprintf(props, sizeof(props), "{\"via\":\"%s\",\"%s\":\"%s\"%s%s,\"line\":%d}", x->via,
|
||||
x->chart ? "chart" : "path", esc, x->tgt > 0 ? "" : ",\"external\":true", ci,
|
||||
x->line);
|
||||
cbm_gbuf_insert_edge(ctx->gbuf, src, tgt, "DEPLOYS", props);
|
||||
edges++;
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, (void *)twins.items);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, spans);
|
||||
cbm_ht_free(files);
|
||||
cbm_ht_free(modules);
|
||||
|
||||
@@ -2119,6 +2119,84 @@ TEST(probe_ci_and_scripts_run_scripts) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The Make model of a project: every Function of a Makefile / .mk as
|
||||
* "fn <file>:<name> <start>-<end>[ +variants]" and every IMPORTS edge of one as
|
||||
* "import <file> -> <label>:<path> (<local_name>)", sorted. */
|
||||
static char *gpf_make_model(const GpfFile *files, int nfiles) {
|
||||
GpfProj lp;
|
||||
cbm_store_t *store = gpf_index_files(&lp, files, nfiles);
|
||||
char *out = NULL;
|
||||
if (store) {
|
||||
sqlite3_stmt *st = NULL;
|
||||
const char *sql =
|
||||
"SELECT coalesce(group_concat(x, char(10)), '') FROM (SELECT x FROM ("
|
||||
"SELECT 'fn ' || file_path || ':' || name || ' ' || start_line || '-' || end_line "
|
||||
"|| CASE WHEN json_extract(properties,'$.variants') IS NULL THEN '' ELSE "
|
||||
"' +variants' END AS x FROM nodes WHERE project=?1 AND label='Function' AND (file_path LIKE "
|
||||
"'%Makefile' OR file_path LIKE '%.mk') UNION ALL SELECT 'import ' || s.file_path || "
|
||||
"' -> ' || t.label || ':' || t.file_path || ' (' || "
|
||||
"json_extract(e.properties,'$.local_name') || ')' FROM edges e JOIN nodes s ON "
|
||||
"s.id=e.source_id JOIN nodes t ON t.id=e.target_id WHERE e.project=?1 AND "
|
||||
"e.type='IMPORTS' AND (s.file_path LIKE '%Makefile' OR s.file_path LIKE '%.mk')) "
|
||||
"ORDER BY x)";
|
||||
if (sqlite3_prepare_v2(cbm_store_get_db(store), sql, -1, &st, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(st, 1, lp.project, -1, SQLITE_STATIC);
|
||||
if (sqlite3_step(st) == SQLITE_ROW) {
|
||||
out = strdup((const char *)sqlite3_column_text(st, 0));
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(st);
|
||||
}
|
||||
gpf_cleanup(&lp, store);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A rule naming several targets (`up down:`, `kind-% seed-%:`) is a node per
|
||||
* target over the rule's span -- `make down` reaches its own node, and the
|
||||
* shared recipe's edges leave from every target. Special targets (.PHONY)
|
||||
* and target-specific variable lines (`up: export X := 1`) are none.
|
||||
* include / -include import the named files (a name holding a
|
||||
* variable stays out), local name the file's base name. */
|
||||
TEST(probe_make_rules_have_a_node_per_target) {
|
||||
static const GpfFile files[] = {
|
||||
{"Makefile", "include mk/tools.mk\n"
|
||||
"-include mk/opt.mk $(VAR)/x.mk\n\n"
|
||||
".PHONY: up down\n"
|
||||
"up down: deps\n"
|
||||
"\t./hack/deploy.sh\n\n"
|
||||
"kind-% seed-%:\n"
|
||||
"\techo pattern\n\n"
|
||||
"single:\n"
|
||||
"\techo one\n\n"
|
||||
"up kind-%: export MODE := fast\n"
|
||||
"only-var: private Y = 1\n"},
|
||||
{"mk/tools.mk", "tool:\n\techo tool\n"},
|
||||
{"mk/opt.mk", "opt:\n\techo opt\n"},
|
||||
{"hack/deploy.sh", "#!/bin/sh\necho deploy\n"},
|
||||
{"hack/ci.sh", "#!/bin/sh\nmake down\n"}};
|
||||
char *got = gpf_make_model(files, 5);
|
||||
const char *want = "fn Makefile:down 5-8\n"
|
||||
"fn Makefile:kind-% 8-11\n"
|
||||
"fn Makefile:seed-% 8-11\n"
|
||||
"fn Makefile:single 11-14\n"
|
||||
"fn Makefile:up 5-8\n"
|
||||
"fn mk/opt.mk:opt 1-3\n"
|
||||
"fn mk/tools.mk:tool 1-3\n"
|
||||
"import Makefile -> Module:mk/opt.mk (opt.mk)\n"
|
||||
"import Makefile -> Module:mk/tools.mk (tools.mk)";
|
||||
if (!got || strcmp(got, want) != 0) {
|
||||
fprintf(stderr, "make model got:\n%s\nwant:\n%s\n", got ? got : "(null)", want);
|
||||
free(got);
|
||||
FAIL("Make model");
|
||||
}
|
||||
free(got);
|
||||
GPF_ASSERT_RUNS(files, 5,
|
||||
"@hack/ci.sh -> rule:Makefile:down (SPAWNS shell)\n"
|
||||
"down -> script:hack/deploy.sh (SPAWNS shell)\n"
|
||||
"up -> script:hack/deploy.sh (SPAWNS shell)");
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* make TARGET SPAWNS the rule of TARGET the makefile reads: the
|
||||
* directory's Makefile (-C, --directory=), a -f file, a rule of a file it
|
||||
* includes; from scripts, CI steps and recipes ($(MAKE)), on the line of
|
||||
@@ -3984,6 +4062,7 @@ SUITE(grammar_probe_f) {
|
||||
RUN_TEST(probe_make_programs_by_path_spawn);
|
||||
RUN_TEST(probe_ci_and_scripts_run_scripts);
|
||||
RUN_TEST(probe_make_targets_spawn_their_rules);
|
||||
RUN_TEST(probe_make_rules_have_a_node_per_target);
|
||||
RUN_TEST(probe_go_installer_deploys_charts);
|
||||
RUN_TEST(probe_terraform_helm_release_deploys_its_chart);
|
||||
RUN_TEST(probe_helm_templated_objects_are_resources);
|
||||
|
||||
Reference in new issue
Block a user