diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e87036..8f6f3771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,12 +18,13 @@ because it turns other people's test suites red. - **A new format: `otlp`, OpenTelemetry traces and logs as OTLP JSON.** `tfg generate --format otlp` writes spans or log records to an exact size, - one batch on each line as the OTLP file exporter writes them, or as one - request for an OTLP endpoint (`formatting`). Settings choose `signal`, the - batch, the start time and rate, trace depth, response codes and severities. - A file of lines comes back from the OpenTelemetry Collector byte for byte, - and none of its lines passes 1 MiB, where the Collector's file receiver - drops a line without saying so. Metrics are not written yet. + one batch on each line as the OTLP file exporter writes them (`.jsonl`), or + one request for an OTLP endpoint (`.json`), chosen by `formatting`. Settings + choose `signal`, batch, start time, rate, trace depth, response codes and + severities. A file of lines comes back from the OpenTelemetry Collector byte + for byte, and no line passes 1 MiB, where the Collector's file receiver + silently drops it. `tfg formats --json` names each ending. Metrics are not + written yet. - **A file can be sent under a name no disk stores.** `send-as` in a recipe, `--send-as` on the command line and Send as in the window take a name such diff --git a/THIRD-PARTY-NOTICES.md b/THIRD-PARTY-NOTICES.md index 1ff36db7..f970e202 100644 --- a/THIRD-PARTY-NOTICES.md +++ b/THIRD-PARTY-NOTICES.md @@ -283,7 +283,7 @@ is the module's own MIT, carried unchanged. | `github.com/jsummers/gobmp` | v0.0.0-20230614200233-a9de23ed2e25 | MIT | (c) 2012-2015 Jason Summers | | `github.com/mattn/go-runewidth` | v0.0.24 | MIT | (c) 2016 Yasuhiro Matsumoto | | `github.com/nfnt/resize` | v0.0.0-20180221191011-83c6a9932646 | ISC | (c) 2012, Jan Schlicht | -| `github.com/nicksnyder/go-i18n/v2` | v2.6.1 | MIT | (c) 2014 Nick Snyder https://github.com/nicksnyder | +| `github.com/nicksnyder/go-i18n/v2` | v2.7.0 | MIT | (c) 2014 Nick Snyder https://github.com/nicksnyder | | `github.com/rymdport/portal` | v0.4.2 | Apache-2.0 | none stated, see below | | `github.com/srwiley/oksvg` | v0.0.0-20221011165216-be6e8873101c | BSD-3-Clause | (c) 2018, Steven R Wiley | | `github.com/srwiley/rasterx` | v0.0.0-20220730225603-2ab79fcdd4ef | BSD-3-Clause | (c) 2018, Steven R Wiley | diff --git a/go.mod b/go.mod index f6fd4fa4..2adb8582 100644 --- a/go.mod +++ b/go.mod @@ -93,7 +93,7 @@ require ( github.com/gen2brain/gav1d v0.2.5 github.com/gen2brain/jxl v0.2.0 github.com/goccy/go-yaml v1.19.2 - github.com/nicksnyder/go-i18n/v2 v2.6.1 + github.com/nicksnyder/go-i18n/v2 v2.7.0 golang.org/x/sys v0.49.0 golang.org/x/text v0.43.0 ) diff --git a/go.sum b/go.sum index 7fec9ea9..e211979a 100644 --- a/go.sum +++ b/go.sum @@ -52,8 +52,8 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= -github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= +github.com/nicksnyder/go-i18n/v2 v2.7.0 h1:oiVQUmuCeNggvm/fUVb6/R0aP82BDVphZnmVHwut+J0= +github.com/nicksnyder/go-i18n/v2 v2.7.0/go.mod h1:ZMYiWg8ndQSGNk4zbzbyJjrrVjQjuQ8pERFrnBik+c8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -68,8 +68,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= golang.org/x/net v0.62.0 h1:18ELalfM5+XNRwzM2nYqXsZQ9ByS1uhj2O/U7+5jqRY= diff --git a/internal/cli/formats.go b/internal/cli/formats.go index 09ca3f59..e5806a9b 100644 --- a/internal/cli/formats.go +++ b/internal/cli/formats.go @@ -6,6 +6,9 @@ import ( "flag" "fmt" "io" + "maps" + "slices" + "strings" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" @@ -65,6 +68,12 @@ type propertyEntry struct { Default string `json:"default,omitempty"` Detail string `json:"detail,omitempty"` Group string `json:"group,omitempty"` + // Extensions are the values of this setting that change what a file's + // name ends in, each with its ending, so a script knows the name before + // the file is made. Absent for every setting that changes no name, which + // is every one a preset, a damage or a tool takes. Added on 2026-11-11, + // which widens this output and changes the meaning of no key in it. + Extensions map[string]string `json:"extensions,omitempty"` } // smallestAccepted is the number this command prints as the minimum. @@ -95,7 +104,7 @@ func entryFor(d format.Descriptor) formatEntry { props := make([]propertyEntry, 0, len(d.Properties)) for _, p := range d.Properties { props = append(props, propertyEntry{ - Name: p.Name, Kind: string(p.Kind), Min: p.Min, Max: p.Max, + Name: p.Name, Kind: string(p.Kind), Min: p.Min, Max: p.Max, Extensions: p.Extensions, Unit: p.Unit, Choices: p.Choices, Default: p.Default, Detail: p.Detail, Group: p.Group, }) @@ -120,7 +129,7 @@ func describeOne(d format.Descriptor, out io.Writer) { fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %s\n", d.ID, d.Fidelity, d.Determinism, core.ExactBytes(smallestAccepted(d))) fmt.Fprintf(out, " name %s\n", d.Name) - fmt.Fprintf(out, " extension %s\n", d.Extension) + fmt.Fprintf(out, " extension %s\n", endings(d)) fmt.Fprintf(out, " padding %s\n", d.Padding.Name) fmt.Fprintf(out, " label %s\n", d.Label) fmt.Fprintf(out, " oracle %s\n", d.Oracle) @@ -151,6 +160,25 @@ func describeOne(d format.Descriptor, out io.Writer) { } } +// endings is the extension a file of d gets, and after it each one a setting +// gives instead with the values that give it: ".jsonl, .json with formatting +// indented or minified". Somebody choosing a layout learns the name before +// the file is made, which is the point of declaring it. +func endings(d format.Descriptor) string { + line := d.Extension + for _, p := range d.Properties { + giving := map[string][]string{} + for value, ext := range p.Extensions { + giving[ext] = append(giving[ext], value) + } + for _, ext := range slices.Sorted(maps.Keys(giving)) { + slices.Sort(giving[ext]) + line += fmt.Sprintf(", %s with %s %s", ext, p.Name, strings.Join(giving[ext], " or ")) + } + } + return line +} + // What a property accepts is asked of the declaration itself, through // Property.Allowed. It used to be worked out here, which put the sentence one // import away from the window - and the window draws its field from the same diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 21b70c63..812dc1e1 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -839,7 +839,9 @@ const indexToken = "{index:04}" // becoming one, across a flag and a preset that answer the same question. var boundaryRoles = [3]string{"under_1b", "at_limit", "over_1b"} -func renderName(t *Target, d format.Descriptor, index int) (string, error) { +// renderName is the name of one file of t, which ends in ext unless the user +// wrote a template. +func renderName(t *Target, ext string, index int) (string, error) { tmpl := t.NameTmpl switch { case tmpl != "": @@ -848,9 +850,9 @@ func renderName(t *Target, d format.Descriptor, index int) (string, error) { // A boundary set says which file is which. The role goes in as literal // text rather than as a token, so it still passes the same name checks // below - the id comes from the user and can hold anything. - tmpl = t.ID + "_" + boundaryRoles[index] + d.Extension + tmpl = t.ID + "_" + boundaryRoles[index] + ext default: - tmpl = t.ID + "_" + indexToken + d.Extension + tmpl = t.ID + "_" + indexToken + ext } name := strings.ReplaceAll(tmpl, indexToken, fmt.Sprintf("%04d", index+1)) diff --git a/internal/engine/plantarget.go b/internal/engine/plantarget.go index d3b85c3c..0b5cd4d1 100644 --- a/internal/engine/plantarget.go +++ b/internal/engine/plantarget.go @@ -35,6 +35,9 @@ type planning struct { // position is the target's place in the recipe, counted from one, and it is // what every refusal below carries so a window can mark the right box. func (pl *planning) files(ctx context.Context, t *Target, desc format.Descriptor, targetSeed uint64, position int) error { + // One ending for every file of the target, because it follows the + // target's settings and every file shares them. + ext := desc.ExtensionFor(t.Properties) for idx, size := range t.Sizes { fileSeed := core.FileSeed(targetSeed, idx) @@ -65,7 +68,7 @@ func (pl *planning) files(ctx context.Context, t *Target, desc format.Descriptor }) } - name, err := renderName(t, desc, idx) + name, err := renderName(t, ext, idx) if err != nil { return atTarget(position, t, err) } diff --git a/internal/format/extension.go b/internal/format/extension.go new file mode 100644 index 00000000..ca574c64 --- /dev/null +++ b/internal/format/extension.go @@ -0,0 +1,41 @@ +package format + +import ( + "maps" + "slices" +) + +// ExtensionFor is what the name of a file of this format ends in when it is +// made with props: the ending the chosen value of a setting names, and +// Extension when none does. +// +// A setting that is absent or empty counts at its default, the way every +// generator reads it, and a default never names an ending of its own - so that +// is Extension too. The value is compared as it is spelled, as Allows compares +// it, so the value the engine accepted is the value looked up here. +// +// The engine asks once for each target rather than once for each file: the +// settings belong to the target, and a target can be a million files. +func (d Descriptor) ExtensionFor(props map[string]string) string { + for _, p := range d.Properties { + if ext, named := p.Extensions[props[p.Name]]; named { + return ext + } + } + return d.Extension +} + +// Extensions is every ending a file of this format can have: Extension first, +// then each one a setting can give instead, in order and once each. It is for +// a surface that lists formats, and has no settings in hand to ask ExtensionFor +// with. +func (d Descriptor) Extensions() []string { + others := map[string]bool{} + for _, p := range d.Properties { + for _, ext := range p.Extensions { + others[ext] = true + } + } + delete(others, d.Extension) + return append([]string{d.Extension}, slices.Sorted(maps.Keys(others))...) +} diff --git a/internal/format/format.go b/internal/format/format.go index 6daf946d..f746c6b6 100644 --- a/internal/format/format.go +++ b/internal/format/format.go @@ -215,6 +215,20 @@ type Property struct { // has (docs/NARZEDZIA-SUMY-2026-09-29.md §14, the owner's decision of // 2026-09-30). Ignored by every kind but text. Long bool + + // Extensions are the values of a choice that change what the name of a + // file ends in, each with the ending it gets - a layout that makes one + // JSON document of a file that is JSON Lines by default. A value left out + // keeps the format's Extension, and so does the default, which is never + // listed: the default's ending has one place to live, and a guard refuses + // a second. + // + // Declared rather than decided by the generator, so the name a file will + // get is known before the file is made: tfg formats prints it, a script + // reads it from --json, and a window shows it in the setting's sentence. + // The engine names every file by Descriptor.ExtensionFor. Ignored by every + // kind but choice. docs/ROZSZERZENIE-UKLADU-2026-11-11.md. + Extensions map[string]string } // Descriptor is everything a format announces about itself. A format missing diff --git a/internal/format/otlp/settings.go b/internal/format/otlp/settings.go index 65c66226..f53b9888 100644 --- a/internal/format/otlp/settings.go +++ b/internal/format/otlp/settings.go @@ -281,8 +281,12 @@ func fileProperties() []format.Property { Name: Formatting, Kind: format.PropertyChoice, Choices: []string{Indented, Minified, RecordPerLine}, Default: RecordPerLine, - Detail: "How the file is laid out. record-per-line puts one batch on each line, which is what the OTLP file exporter writes and the file receiver of the OpenTelemetry Collector reads. " + - "minified and indented make the whole file one request, the body to post to an OTLP endpoint, and a file of that kind is one JSON document whatever its name ends in. " + + // A file of one request is one JSON document, and .jsonl would + // tell a reader that picks its parser by the name to read it line + // by line. docs/ROZSZERZENIE-UKLADU-2026-11-11.md. + Extensions: map[string]string{Indented: ".json", Minified: ".json"}, + Detail: "How the file is laid out. record-per-line puts one batch on each line, which is what the OTLP file exporter writes and the file receiver of the OpenTelemetry Collector reads, and the file name ends in .jsonl. " + + "minified and indented make the whole file one request, the body to post to an OTLP endpoint, and the file name ends in .json. " + "Posted as a body, a file of several lines loses every line after the first in the Collector, and its file receiver reads nothing from an indented file.", }, { diff --git a/internal/guard/extensionbylayout_test.go b/internal/guard/extensionbylayout_test.go new file mode 100644 index 00000000..cb79927a --- /dev/null +++ b/internal/guard/extensionbylayout_test.go @@ -0,0 +1,216 @@ +package guard + +import ( + "encoding/json" + "maps" + "path/filepath" + "regexp" + "slices" + "strings" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/cli" + "github.com/donislawdev/TestingFilesGenerator/internal/engine" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" + "github.com/donislawdev/TestingFilesGenerator/internal/format/otlp" +) + +// An otlp file in a layout of one request is one JSON document, and until +// 2026-11-11 its name ended in .jsonl whatever the layout - which tells a reader +// that picks its parser by the name to read the file line by line. A setting +// now declares the endings its values give (format.Property.Extensions), and +// the engine names every file by Descriptor.ExtensionFor. +// docs/ROZSZERZENIE-UKLADU-2026-11-11.md. + +// TestEveryEndingASettingGivesIsDeclaredSoItCanBeTrusted holds the declarations +// to what ExtensionFor, tfg formats and the site lean on. +func TestEveryEndingASettingGivesIsDeclaredSoItCanBeTrusted(t *testing.T) { + declaring := 0 + for _, d := range format.All() { + settings := 0 + for _, p := range d.Properties { + if len(p.Extensions) == 0 { + continue + } + settings++ + declaring++ + endingsAreDeclaredWell(t, d, p) + } + if settings > 1 { + t.Errorf("%s: %d settings change what a file name ends in, and ExtensionFor takes the first that matches - "+ + "one setting may, so the name never depends on the order the settings are declared in", d.ID, settings) + } + } + // otlp's formatting gives one. Without this a rule taken out leaves the + // loop above with nothing to look at, green. + if declaring == 0 { + t.Fatal("no setting of any format changes what a file name ends in, so this guard checked nothing - otlp's formatting did") + } +} + +func endingsAreDeclaredWell(t *testing.T, d format.Descriptor, p format.Property) { + t.Helper() + if p.Kind != format.PropertyChoice { + t.Errorf("%s %s: only a choice can give a file name its ending, and this is a %s", d.ID, p.Name, p.Kind) + } + for value, ext := range p.Extensions { + switch { + case !slices.Contains(p.Choices, value): + t.Errorf("%s %s: %q gives %s and is not one of the values it takes, %v", d.ID, p.Name, value, ext, p.Choices) + case value == p.Default: + t.Errorf("%s %s: the default %q is listed with %s - the default's ending is Extension, and a second place for it is one that goes stale", + d.ID, p.Name, value, ext) + case !strings.HasPrefix(ext, "."): + t.Errorf("%s %s: %q gives %q, which does not start with a dot", d.ID, p.Name, value, ext) + case ext == d.Extension: + t.Errorf("%s %s: %q gives %s, the format's own ending, so the entry changes nothing", d.ID, p.Name, value, ext) + } + } + // The sentence under the setting is where a window says it, and it is + // written by hand beside the map - so it is held to the map. Matched as a + // whole ending, because .json is also the first five letters of .jsonl. + for _, ext := range d.Extensions() { + if !regexp.MustCompile(regexp.QuoteMeta(ext) + `\b`).MatchString(p.Detail) { + t.Errorf("%s %s: a file can end in %s, and the sentence under the setting never says so: %q", d.ID, p.Name, ext, p.Detail) + } + } +} + +// TestEveryFileIsNamedForTheSettingsItWasMadeWith plans files the way a run +// plans them and reads the names the engine gave. +// +// otlp is asked first with its endings written out. The loop after it compares +// with ExtensionFor, so it is what catches an engine that stops asking - and +// only the written endings catch an ExtensionFor that stops answering. +func TestEveryFileIsNamedForTheSettingsItWasMadeWith(t *testing.T) { + for layout, want := range map[string]string{ + "": ".jsonl", + otlp.RecordPerLine: ".jsonl", + otlp.Minified: ".json", + otlp.Indented: ".json", + } { + props := map[string]string{} + if layout != "" { + props[otlp.Formatting] = layout + } + for _, name := range plannedNames(t, "otlp", props, "") { + if got := filepath.Ext(name); got != want { + t.Errorf("otlp with formatting %q: %s ends in %s, and a file of this layout ends in %s", layout, name, got, want) + } + } + } + + // A template somebody wrote wins, ending and all - a test of a form that + // reads only the ending asks for exactly that. + for _, name := range plannedNames(t, "otlp", map[string]string{otlp.Formatting: otlp.Indented}, "mine_{index:04}.jsonl") { + if !strings.HasPrefix(name, "mine_") || filepath.Ext(name) != ".jsonl" { + t.Errorf("a template asked for mine_{index:04}.jsonl and the file was named %s", name) + } + } + + checked := 0 + for _, d := range format.All() { + for _, p := range d.Properties { + if len(p.Extensions) == 0 { + continue + } + for _, value := range append([]string{""}, p.Choices...) { + props := map[string]string{} + if value != "" { + props[p.Name] = value + } + want := d.ExtensionFor(props) + for _, name := range plannedNames(t, d.ID, props, "") { + if !strings.HasSuffix(name, want) { + t.Errorf("%s with %s %q: %s does not end in %s", d.ID, p.Name, value, name, want) + } + checked++ + } + } + } + } + if checked == 0 { + t.Fatal("no file was named under a setting that gives an ending, so the loop proved nothing") + } +} + +// TestTheFormatsCommandSaysWhichSettingGivesWhichEnding holds both outputs of +// tfg formats to the declaration: the line a person reads and the key a script +// reads. The names the engine gives are held above, and nothing there would +// notice either output saying .jsonl alone (CodeRabbit on #231, 2026-11-11). +func TestTheFormatsCommandSaysWhichSettingGivesWhichEnding(t *testing.T) { + code, text, errOut := run(t, "formats", "otlp") + if code != cli.ExitOK { + t.Fatalf("tfg formats otlp ended %d: %s", code, errOut) + } + // Written out, because the line is what a person reads before choosing a + // layout - built from the declaration it would agree with any declaration. + if want := " extension .jsonl, .json with formatting indented or minified\n"; !strings.Contains(text, want) { + t.Errorf("tfg formats otlp does not say which layout gives .json - it should hold %q:\n%s", want, text) + } + + checked := 0 + for _, d := range format.All() { + declared := map[string]map[string]string{} + for _, p := range d.Properties { + if len(p.Extensions) > 0 { + declared[p.Name] = p.Extensions + } + } + if len(declared) == 0 { + continue + } + code, out, errOut := run(t, "formats", d.ID, "--json") + if code != cli.ExitOK { + t.Fatalf("tfg formats %s --json ended %d: %s", d.ID, code, errOut) + } + var list []struct { + Properties []struct { + Name string `json:"name"` + Extensions map[string]string `json:"extensions"` + } `json:"properties"` + } + if err := json.Unmarshal([]byte(out), &list); err != nil || len(list) != 1 { + t.Fatalf("tfg formats %s --json is not one format: %v\n%s", d.ID, err, out) + } + for _, p := range list[0].Properties { + if want := declared[p.Name]; !maps.Equal(p.Extensions, want) { + t.Errorf("tfg formats %s --json gives %s the endings %v, and it declares %v", d.ID, p.Name, p.Extensions, want) + } + checked++ + } + } + if checked == 0 { + t.Fatal("no format declares an ending a setting gives, so --json was never read - otlp's formatting did") + } +} + +// plannedNames plans two files of format id and, with no template, a boundary +// set beside them - the two ways the engine names a file itself - and gives +// back every name. +func plannedNames(t *testing.T, id string, props map[string]string, tmpl string) []string { + t.Helper() + d, err := format.Get(id) + if err != nil { + t.Fatal(err) + } + size := d.SmallestAccepted(format.Request{Label: true, Properties: props}) + targets := []engine.Target{{ID: "files", Format: id, Sizes: []int64{size, size}, Label: true, Properties: props, NameTmpl: tmpl}} + if tmpl == "" { + targets = append(targets, engine.Target{ID: "edge", Format: id, Sizes: []int64{size, size + 1, size + 2}, + BoundaryLimit: size + 1, Label: true, Properties: props}) + } + planned, err := engine.Plan(targets, engine.Options{OutDir: t.TempDir(), Seed: 7, Command: "test"}) + if err != nil { + t.Fatalf("%s with %v: %v", id, props, err) + } + names := make([]string, 0, len(planned)) + for _, f := range planned { + names = append(names, f.Name) + } + if want := 2 + 3*min(1, len(targets)-1); len(names) != want { + t.Fatalf("%s with %v: %d files were planned and %d were asked for", id, props, len(names), want) + } + return names +} diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 0d4d4319..a0c413b5 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -17670,4 +17670,39 @@ " doc = json.loads(text)", "TestTheOTLPCheckRefusesWhatAJSONReaderLetsThrough", ), + ( + "the engine names every file by the format's own extension, whatever was chosen", + "internal/engine/plantarget.go", + "ext := desc.ExtensionFor(t.Properties)", + "ext := desc.Extension", + "TestEveryFileIsNamedForTheSettingsItWasMadeWith", + ), + ( + "ExtensionFor answers the format's own extension, whatever was chosen", + "internal/format/extension.go", + "if ext, named := p.Extensions[props[p.Name]]; named {", + "if ext, named := p.Extensions[props[p.Name]]; named && false {", + "TestEveryFileIsNamedForTheSettingsItWasMadeWith", + ), + ( + "otlp in a layout of one request keeps the .jsonl ending", + "internal/format/otlp/settings.go", + "\t\t\tExtensions: map[string]string{Indented: \".json\", Minified: \".json\"},\n", + "", + "TestEveryEndingASettingGivesIsDeclaredSoItCanBeTrusted", + ), + ( + "tfg formats --json stops carrying the endings a setting gives", + "internal/cli/formats.go", + "Max: p.Max, Extensions: p.Extensions,", + "Max: p.Max,", + "TestTheFormatsCommandSaysWhichSettingGivesWhichEnding", + ), + ( + "the card of one format gives its extension alone, whatever a setting changes", + "internal/cli/formats.go", + "fmt.Fprintf(out, \" extension %s\\n\", endings(d))", + "fmt.Fprintf(out, \" extension %s\\n\", d.Extension)", + "TestTheFormatsCommandSaysWhichSettingGivesWhichEnding", + ), ] diff --git a/internal/guard/site_test.go b/internal/guard/site_test.go index 73464f77..836051f7 100644 --- a/internal/guard/site_test.go +++ b/internal/guard/site_test.go @@ -102,7 +102,7 @@ func factsFromTheProgram(t *testing.T) site.Facts { formats = append(formats, site.Format{ ID: d.ID, Name: d.Name, - Extension: d.Extension, + Extensions: d.Extensions(), Fidelity: string(d.Fidelity), Determinism: string(d.Determinism), // The same number the command line prints, from the same call. diff --git a/internal/gui/text/locale/registry/en.json b/internal/gui/text/locale/registry/en.json index ce9ed2f1..f1a839be 100644 --- a/internal/gui/text/locale/registry/en.json +++ b/internal/gui/text/locale/registry/en.json @@ -1596,8 +1596,8 @@ }, "Detail.format/otlp.formatting": { "description": "The sentence under the formatting setting of the otlp format, after what it takes.", - "hash": "sha256-a0bda4ebdc96", - "other": "How the file is laid out. record-per-line puts one batch on each line, which is what the OTLP file exporter writes and the file receiver of the OpenTelemetry Collector reads. minified and indented make the whole file one request, the body to post to an OTLP endpoint, and a file of that kind is one JSON document whatever its name ends in. Posted as a body, a file of several lines loses every line after the first in the Collector, and its file receiver reads nothing from an indented file." + "hash": "sha256-b094d2e4c7a2", + "other": "How the file is laid out. record-per-line puts one batch on each line, which is what the OTLP file exporter writes and the file receiver of the OpenTelemetry Collector reads, and the file name ends in .jsonl. minified and indented make the whole file one request, the body to post to an OTLP endpoint, and the file name ends in .json. Posted as a body, a file of several lines loses every line after the first in the Collector, and its file receiver reads nothing from an indented file." }, "Detail.format/otlp.level_mix": { "description": "The sentence under the level_mix setting of the otlp format, after what it takes.", diff --git a/internal/gui/text/locale/registry/pl.json b/internal/gui/text/locale/registry/pl.json index 2dac251d..fe734e1b 100644 --- a/internal/gui/text/locale/registry/pl.json +++ b/internal/gui/text/locale/registry/pl.json @@ -318,7 +318,7 @@ "Detail.format/mp4.width": { "hash": "sha256-3085ac44f0e0", "other": "Jak szeroki jest obraz. Gdy pole zostaje puste, wybierany jest rozmiar, który mieści się w zamówionych bajtach." }, "Detail.format/otlp.batch": { "hash": "sha256-3c2c865d96f5", "other": "Ile spanów albo rekordów logów ma każda linia. 512 to paczka, którą SDK OpenTelemetry wysyła domyślnie. Paczki ma tylko układ rekord na wiersz, a przy innym układzie paczka jest odrzucana." }, "Detail.format/otlp.depth": { "hash": "sha256-d1caceabbb7a", "other": "Ile spanów ma każdy ślad, każdy jako dziecko poprzedniego. 1 zostawia każdy span bez rodzica. Kształtuje spany, a przy sygnale logi jest odrzucane." }, - "Detail.format/otlp.formatting": { "hash": "sha256-a0bda4ebdc96", "other": "Jak ułożony jest plik. Przy rekord na wiersz każda paczka stoi w osobnej linii, tak jak zapisuje ją eksporter plików OTLP i jak czyta ją odbiornik plików OpenTelemetry Collectora. Przy zminifikowany i z wcięciami cały plik jest jednym żądaniem, czyli treścią do wysłania na endpoint OTLP, i taki plik jest jednym dokumentem JSON niezależnie od końcówki nazwy. Wysłany jako treść żądania plik o kilku liniach traci w Collectorze każdą linię po pierwszej, a jego odbiornik plików nie czyta nic z pliku z wcięciami." }, + "Detail.format/otlp.formatting": { "hash": "sha256-b094d2e4c7a2", "other": "Jak ułożony jest plik. Przy rekord na wiersz każda paczka stoi w osobnej linii, tak jak zapisuje ją eksporter plików OTLP i jak czyta ją odbiornik plików OpenTelemetry Collectora, a nazwa pliku kończy się na .jsonl. Przy zminifikowany i z wcięciami cały plik jest jednym żądaniem, czyli treścią do wysłania na endpoint OTLP, a nazwa pliku kończy się na .json. Wysłany jako treść żądania plik o kilku liniach traci w Collectorze każdą linię po pierwszej, a jego odbiornik plików nie czyta nic z pliku z wcięciami." }, "Detail.format/otlp.level_mix": { "hash": "sha256-96679b162c65", "other": "Jakie poziomy ważności niosą rekordy logów. Realistycznie daje głównie INFO i trochę pozostałych. Kształtuje rekordy logów, a przy sygnale ślady jest odrzucane." }, "Detail.format/otlp.rate": { "hash": "sha256-ce402f8f7753", "other": "Ile śladów albo rekordów logów zaczyna się w każdej sekundzie." }, "Detail.format/otlp.signal": { "hash": "sha256-a0541e2d1349", "other": "Jaki rodzaj telemetrii jest w pliku. Przy ślady plik niesie spany, a przy logi rekordy logów. Plik niesie jeden rodzaj, tak jak wymaga tego format plików OTLP." }, diff --git a/internal/site/site.go b/internal/site/site.go index 3aa22f91..4a643fca 100644 --- a/internal/site/site.go +++ b/internal/site/site.go @@ -59,8 +59,11 @@ type Format struct { // every page. A proper name is not translated, which is why the registry // can own it - and the two that describe rather than name (plain text and // a log) are the known exception, decided on 2026-09-24. - Name string - Extension string + Name string + // Extensions is every ending a file of the format can have, the usual + // one first - otlp writes .jsonl, and .json in a layout that makes one + // JSON document. Which setting gives which is said by tfg formats. + Extensions []string Fidelity string Determinism string Smallest int64 diff --git a/web/public/ar/formats/index.html b/web/public/ar/formats/index.html index 24f9cda3..a71c5d54 100644 --- a/web/public/ar/formats/index.html +++ b/web/public/ar/formats/index.html @@ -286,7 +286,7 @@
otlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .jsonotlp.jsonl.jsonl .json{{ .ID }}{{ .Extension }}{{ $e }}{{ end }}