Skip to content

Commit 8b7e7f7

Browse files
authored
Merge pull request #22 from ncode/juliano/deepen-discovery-input-surface
Deepen discovery input surface
2 parents 1609798 + 4f97d12 commit 8b7e7f7

14 files changed

Lines changed: 559 additions & 71 deletions

File tree

‎cache_test.go‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package facts
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"os"
78
"path/filepath"
89
"testing"
@@ -111,6 +112,34 @@ func TestWithCache_servesFreshCachedValueOverResolver(t *testing.T) {
111112
}
112113
}
113114

115+
func TestWithCache_selectsQueriedFactsThroughEngineCachePath(t *testing.T) {
116+
dir := redirectCacheDir(t)
117+
conf := writeTTLConfig(t, "demo")
118+
seedCacheFile(t, filepath.Join(dir, "demo"), map[string]any{"demo": map[string]any{"child": "from-cache"}})
119+
120+
eng, err := New(
121+
WithCache(),
122+
WithConfigFile(conf),
123+
WithFact("demo", func(context.Context) (any, error) {
124+
return map[string]any{"child": "fresh"}, nil
125+
}),
126+
WithFact("other", cachingResolver("fresh")),
127+
)
128+
if err != nil {
129+
t.Fatal(err)
130+
}
131+
snap, err := eng.Discover(context.Background(), "demo.child")
132+
if err != nil {
133+
t.Fatalf("Discover() err = %v", err)
134+
}
135+
if got, err := snap.Value("demo.child"); err != nil || got != "from-cache" {
136+
t.Fatalf("Value(demo.child) = %#v, %v, want queried cached value", got, err)
137+
}
138+
if _, err := snap.Value("other"); !errors.Is(err, ErrFactNotFound) {
139+
t.Fatalf("Value(other) err = %v, want unqueried fact omitted from cached Snapshot", err)
140+
}
141+
}
142+
114143
// TestWithoutCache_ignoresExistingCache is the toggle control: the same seeded
115144
// cache that WithCache would serve is ignored when WithCache is absent, proving
116145
// the option — not some always-on path — is what enables caching.

‎engine_contract_test.go‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,26 @@ func TestSnapshotTree_excludesLegacyAliasFacts(t *testing.T) {
130130
}
131131
}
132132

133+
func TestDiscoverQueriesSelectReturnedSnapshotFacts(t *testing.T) {
134+
dir := t.TempDir()
135+
writeTestFile(t, dir, "site.txt", "site_one=one\nsite_two=two\n")
136+
137+
eng, err := New(WithExternalDirs(dir))
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
snap, err := eng.Discover(context.Background(), "site_one")
142+
if err != nil {
143+
t.Fatalf("Discover() err = %v", err)
144+
}
145+
if got, err := snap.Value("site_one"); err != nil || got != "one" {
146+
t.Fatalf("Value(site_one) = %#v, %v, want selected fact", got, err)
147+
}
148+
if _, err := snap.Value("site_two"); !errors.Is(err, ErrFactNotFound) {
149+
t.Fatalf("Value(site_two) err = %v, want unqueried fact omitted from selected Snapshot", err)
150+
}
151+
}
152+
133153
// Pins the surviving halves of TestAdd_resolvesProgrammaticCustomFactLazily,
134154
// TestValue_reusesResolvedProgrammaticCustomFact, and
135155
// TestValue_missingFactDoesNotResolveUnrelatedProgrammaticCustomFacts: each
@@ -155,8 +175,8 @@ func TestWithFact_resolverRunsExactlyOncePerDiscover(t *testing.T) {
155175
t.Fatalf("resolver ran %d times after one Discover with an unrelated query, want eager resolution exactly once", calls)
156176
}
157177
for range 2 {
158-
if got, err := first.Value("counted_fact"); err != nil || got != 1 {
159-
t.Fatalf("Value(counted_fact) = %#v, %v, want first-run value 1", got, err)
178+
if _, err := first.Value("counted_fact"); !errors.Is(err, ErrFactNotFound) {
179+
t.Fatalf("Value(counted_fact) err = %v, want unrelated fact omitted from queried Snapshot", err)
160180
}
161181
}
162182
if calls != 1 {
@@ -173,8 +193,8 @@ func TestWithFact_resolverRunsExactlyOncePerDiscover(t *testing.T) {
173193
if got, _ := second.Value("counted_fact"); got != 2 {
174194
t.Fatalf("second Value(counted_fact) = %#v, want fresh value 2", got)
175195
}
176-
if got, _ := first.Value("counted_fact"); got != 1 {
177-
t.Fatalf("first Value(counted_fact) = %#v after rediscovery, want immutable 1", got)
196+
if _, err := first.Value("counted_fact"); !errors.Is(err, ErrFactNotFound) {
197+
t.Fatalf("first Value(counted_fact) err = %v after rediscovery, want queried Snapshot unchanged", err)
178198
}
179199
}
180200

‎engine_test.go‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,68 @@ func TestWithConfigFile_loadsConfiguredDirs(t *testing.T) {
119119
}
120120
}
121121

122+
func TestWithConfigFile_recomputesDiscoveryPolicyEachDiscover(t *testing.T) {
123+
cacheDir := redirectCacheDir(t)
124+
firstDir := t.TempDir()
125+
writeTestFile(t, firstDir, "site.txt", "site_location=first\n")
126+
writeTestFile(t, firstDir, "blocked.txt", "blocked_probe=blocked\n")
127+
secondDir := t.TempDir()
128+
writeTestFile(t, secondDir, "site.txt", "site_location=second\nblocked_probe=visible\n")
129+
configDir := t.TempDir()
130+
configPath := filepath.Join(configDir, "facter.conf")
131+
writeTestFile(t, configDir, "facter.conf", `global : {
132+
external-dir : [ "`+firstDir+`" ],
133+
}
134+
facts : {
135+
blocklist : [ "blocked.txt" ],
136+
}
137+
`)
138+
139+
eng, err := New(
140+
WithCache(),
141+
WithConfigFile(configPath),
142+
WithFact("cache_probe", func(context.Context) (any, error) { return "cached", nil }),
143+
)
144+
if err != nil {
145+
t.Fatal(err)
146+
}
147+
first, err := eng.Discover(context.Background())
148+
if err != nil {
149+
t.Fatalf("first Discover() err = %v", err)
150+
}
151+
if got, err := first.Value("site_location"); err != nil || got != "first" {
152+
t.Fatalf("first Value(site_location) = %#v, %v, want first", got, err)
153+
}
154+
if _, err := first.Value("blocked_probe"); !errors.Is(err, ErrFactNotFound) {
155+
t.Fatalf("first Value(blocked_probe) err = %v, want ErrFactNotFound", err)
156+
}
157+
if _, err := os.Stat(filepath.Join(cacheDir, "cache_probe")); !os.IsNotExist(err) {
158+
t.Fatalf("cache file after first Discover stat err = %v, want not exist", err)
159+
}
160+
161+
writeTestFile(t, configDir, "facter.conf", `global : {
162+
external-dir : [ "`+secondDir+`" ],
163+
}
164+
facts : {
165+
ttls : [ { "cache_probe" : "30 days" } ],
166+
}
167+
`)
168+
second, err := eng.Discover(context.Background())
169+
if err != nil {
170+
t.Fatalf("second Discover() err = %v", err)
171+
}
172+
if got, err := second.Value("site_location"); err != nil || got != "second" {
173+
t.Fatalf("second Value(site_location) = %#v, %v, want second", got, err)
174+
}
175+
if got, err := second.Value("blocked_probe"); err != nil || got != "visible" {
176+
t.Fatalf("second Value(blocked_probe) = %#v, %v, want visible", got, err)
177+
}
178+
data := readCacheFile(t, filepath.Join(cacheDir, "cache_probe"))
179+
if data["cache_probe"] != "cached" {
180+
t.Fatalf("cached cache_probe = %#v, want cached", data["cache_probe"])
181+
}
182+
}
183+
122184
func TestWithConfigFile_blocklistSuppressesFacts(t *testing.T) {
123185
configPath := writeTestFile(t, t.TempDir(), "facter.conf", "facts : {\n blocklist : [ \"ssh\" ],\n}\n")
124186

‎internal/app/app.go‎

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,10 @@ func runQuery(stdout, stderr io.Writer, args []string) error {
264264
if configErr != nil {
265265
return configErr
266266
}
267-
if len(externalDirs) == 0 {
268-
externalDirs = configOptions.ExternalDirs
267+
cliExternalDirs := externalDirs
268+
discoveryExternalDirs := externalDirs
269+
if len(discoveryExternalDirs) == 0 {
270+
discoveryExternalDirs = configOptions.ExternalDirs
269271
}
270272
if configOptions.NoExternalFacts {
271273
*noExternalFacts = true
@@ -276,16 +278,21 @@ func runQuery(stdout, stderr io.Writer, args []string) error {
276278
if configOptions.Verbose {
277279
*verbose = true
278280
}
279-
if *noExternalFacts && hasNonEmpty(externalDirs) {
281+
if *noExternalFacts && hasNonEmpty(discoveryExternalDirs) {
280282
return optionError(stdout, errors.New("--no-external-facts and --external-dir options conflict: please specify only one"))
281283
}
282284
if !*noExternalFacts {
283-
externalDirs = effectiveExternalDirs(externalDirs)
285+
discoveryExternalDirs = effectiveExternalDirs(discoveryExternalDirs)
286+
}
287+
blockedFactsForFastPath := map[string]bool{}
288+
var blockedFacts map[string]bool
289+
if *noBlock {
290+
blockedFacts = map[string]bool{}
284291
}
285-
blockedFacts := map[string]bool{}
286292
if !*noBlock {
287-
blockedFacts = engine.BlocklistedFactsForFiltering(configOptions.Blocklist, configOptions.FactGroups)
293+
blockedFactsForFastPath = engine.BlocklistedFactsForFiltering(configOptions.Blocklist, configOptions.FactGroups)
288294
}
295+
mergeDottedFacts := configOptions.ForceDotResolution || *forceDotResolution
289296
logLevel := firstNonEmpty(flags.Lookup("log-level").Value.String(), flags.Lookup("l").Value.String(), configOptions.LogLevel)
290297
if logLevel != "" && !cli.SupportedLogLevel(logLevel) {
291298
return optionError(stdout, fmt.Errorf("unsupported log level %s", logLevel))
@@ -304,17 +311,25 @@ func runQuery(stdout, stderr io.Writer, args []string) error {
304311
writeInfo(stderr, "executed with command line: "+strings.Join(args, " "), colorDiagnostics)
305312
writeInfo(stderr, "resolving facts", colorDiagnostics)
306313
}
307-
if canUseVersionQueryFastPath(flags.Args(), externalDirs, blockedFacts, *noExternalFacts, *timing || *timingShort) {
314+
if canUseVersionQueryFastPath(flags.Args(), discoveryExternalDirs, blockedFactsForFastPath, *noExternalFacts, *timing || *timingShort) {
308315
return writeVersionQuery(stdout, *jsonOutput || *jsonOutputShort, *yamlOutput || *yamlOutputShort, *hoconOutput)
309316
}
310317
resolutionStart := time.Now()
311318

312319
eng, err := engine.NewEngine(engine.EngineConfig{
313-
CLICompat: true,
314-
ExternalDirs: externalDirs,
315-
NoExternalFacts: *noExternalFacts,
316-
BlockedFacts: blockedFacts,
317-
Logger: logger,
320+
CLICompat: true,
321+
SystemDefaults: true,
322+
ConfigFile: configFile,
323+
ConfigLoaded: true,
324+
Config: configOptions,
325+
ExternalDirs: cliExternalDirs,
326+
UseCache: !*noCache,
327+
NoExternalFacts: *noExternalFacts,
328+
BlockedFacts: blockedFacts,
329+
DefaultExternalDirsSet: true,
330+
DefaultExternalDirs: defaultExternalFactDirs(),
331+
IncludeTypedDotted: mergeDottedFacts,
332+
Logger: logger,
318333
})
319334
if err != nil {
320335
return err
@@ -324,17 +339,6 @@ func runQuery(stdout, stderr io.Writer, args []string) error {
324339
return err
325340
}
326341
facts := snapshot.Facts()
327-
mergeDottedFacts := configOptions.ForceDotResolution || *forceDotResolution
328-
projection := engine.NewProjection(facts, mergeDottedFacts)
329-
facts = projection.Select(flags.Args())
330-
if !*noCache {
331-
cache := engine.NewFactCache(engine.DefaultCachePath(), configOptions.TTLs, configOptions.FactGroups, logger)
332-
remaining, cached := cache.ResolveFacts(facts)
333-
if err := cache.CacheFacts(remaining); err != nil {
334-
return err
335-
}
336-
facts = append(remaining, cached...)
337-
}
338342
resolutionDuration := time.Since(resolutionStart).Seconds()
339343
out, err := engine.BuildFormatter(engine.FormatOptions{
340344
JSON: *jsonOutput || *jsonOutputShort,

‎internal/app/app_test.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@ func TestMain(m *testing.M) {
1818
os.Exit(m.Run())
1919
}
2020

21+
func seedAppCacheFile(t *testing.T, path string, data map[string]any) {
22+
t.Helper()
23+
data["cache_format_version"] = 1
24+
raw, err := json.Marshal(data)
25+
if err != nil {
26+
t.Fatal(err)
27+
}
28+
if err := os.WriteFile(path, raw, 0o600); err != nil {
29+
t.Fatal(err)
30+
}
31+
}
32+
2133
func TestRun_version(t *testing.T) {
2234
var stdout, stderr bytes.Buffer
2335

@@ -535,6 +547,35 @@ func TestRun_configTTLsUseFreshCachedFact(t *testing.T) {
535547
}
536548
}
537549

550+
func TestRun_noCacheIgnoresFreshCachedFact(t *testing.T) {
551+
dir := t.TempDir()
552+
factPath := filepath.Join(dir, "site.txt")
553+
if err := os.WriteFile(factPath, []byte("site_role=fresh\n"), 0o600); err != nil {
554+
t.Fatal(err)
555+
}
556+
configPath := filepath.Join(t.TempDir(), "facter.conf")
557+
conf := "facts : {\n ttls : [ { \"site_role\" : \"1 hour\" } ],\n}\n"
558+
if err := os.WriteFile(configPath, []byte(conf), 0o600); err != nil {
559+
t.Fatal(err)
560+
}
561+
cacheDir := t.TempDir()
562+
oldDefaultCachePath := engine.DefaultCachePath
563+
engine.DefaultCachePath = func() string { return cacheDir }
564+
t.Cleanup(func() { engine.DefaultCachePath = oldDefaultCachePath })
565+
seedAppCacheFile(t, filepath.Join(cacheDir, "site_role"), map[string]any{"site_role": "from-cache"})
566+
var stdout, stderr bytes.Buffer
567+
568+
if err := Run(&stdout, &stderr, []string{"--no-cache", "--config", configPath, "--external-dir", dir, "site_role"}); err != nil {
569+
t.Fatal(err)
570+
}
571+
if got, want := stdout.String(), "fresh\n"; got != want {
572+
t.Fatalf("stdout = %q, want %q", got, want)
573+
}
574+
if stderr.Len() != 0 {
575+
t.Fatalf("stderr = %q, want empty", stderr.String())
576+
}
577+
}
578+
538579
func TestRun_strictLogsMissingFactErrorWhenQueriedFactIsMissing(t *testing.T) {
539580
var stdout, stderr bytes.Buffer
540581

0 commit comments

Comments
 (0)