From dbda2008464efe3a665f711d572fb71d01dc0690 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 11 Nov 2026 13:54:01 +0100 Subject: [PATCH 1/3] An otlp file of one request ends in .json, and tfg formats names each ending An otlp file in the minified or indented layout is one JSON document, and its name ended in .jsonl - which tells a reader that picks its parser by the name to read it line by line. A setting can now declare the endings its values give (format.Property.Extensions), and the engine names every file by Descriptor.ExtensionFor, once for each target. The bytes of every file are unchanged, and a name template somebody wrote still wins. tfg formats otlp prints the endings beside the extension, tfg formats --json carries them as "extensions" on the setting, the site lists both endings, and the sentence under formatting says them. Two guards hold the declaration and the names the engine plans, with three mutation entries. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +- internal/cli/formats.go | 32 ++++- internal/engine/engine.go | 8 +- internal/engine/plantarget.go | 5 +- internal/format/extension.go | 41 ++++++ internal/format/format.go | 14 ++ internal/format/otlp/settings.go | 8 +- internal/guard/extensionbylayout_test.go | 162 ++++++++++++++++++++++ internal/guard/mutation/entries.py | 21 +++ internal/guard/site_test.go | 2 +- internal/gui/text/locale/registry/en.json | 4 +- internal/gui/text/locale/registry/pl.json | 2 +- internal/site/site.go | 7 +- web/public/ar/formats/index.html | 2 +- web/public/cs/formaty/index.html | 2 +- web/public/de/formate/index.html | 2 +- web/public/es/formatos/index.html | 2 +- web/public/formats/index.html | 2 +- web/public/fr/formats/index.html | 2 +- web/public/hi/formats/index.html | 2 +- web/public/id/format/index.html | 2 +- web/public/it/formati/index.html | 2 +- web/public/ja/formats/index.html | 2 +- web/public/ko/formats/index.html | 2 +- web/public/nl/formaten/index.html | 2 +- web/public/pl/formaty/index.html | 2 +- web/public/pt-br/formatos/index.html | 2 +- web/public/ro/formate/index.html | 2 +- web/public/ru/formats/index.html | 2 +- web/public/th/formats/index.html | 2 +- web/public/tr/bicimler/index.html | 2 +- web/public/uk/formats/index.html | 2 +- web/public/vi/dinh-dang/index.html | 2 +- web/public/zh-hans/formats/index.html | 2 +- web/public/zh-hant/formats/index.html | 2 +- web/templates/partials.html | 2 +- 36 files changed, 322 insertions(+), 43 deletions(-) create mode 100644 internal/format/extension.go create mode 100644 internal/guard/extensionbylayout_test.go 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/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..63fd8d0f --- /dev/null +++ b/internal/guard/extensionbylayout_test.go @@ -0,0 +1,162 @@ +package guard + +import ( + "path/filepath" + "regexp" + "slices" + "strings" + "testing" + + "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") + } +} + +// 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..99d81a97 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -17670,4 +17670,25 @@ " 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", + ), ] 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 @@

32 صيغة ملف، كلٌّ منها يُولَّد بحجم دقيق

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/cs/formaty/index.html b/web/public/cs/formaty/index.html index 64f568e1..177b5464 100644 --- a/web/public/cs/formaty/index.html +++ b/web/public/cs/formaty/index.html @@ -286,7 +286,7 @@

32 formátů souborů, každý generovaný v přesné velikosti

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/de/formate/index.html b/web/public/de/formate/index.html index d10f1dec..b9cd6c0e 100644 --- a/web/public/de/formate/index.html +++ b/web/public/de/formate/index.html @@ -287,7 +287,7 @@

32 Dateiformate, jedes in exakter Größe erzeugt

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/es/formatos/index.html b/web/public/es/formatos/index.html index 924b0eed..88f4ea47 100644 --- a/web/public/es/formatos/index.html +++ b/web/public/es/formatos/index.html @@ -287,7 +287,7 @@

32 formatos de archivo, cada uno generado a un tamaño exacto

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/formats/index.html b/web/public/formats/index.html index b1f7c5df..ca75e80d 100644 --- a/web/public/formats/index.html +++ b/web/public/formats/index.html @@ -287,7 +287,7 @@

32 file formats, every one generated at an exact size

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/fr/formats/index.html b/web/public/fr/formats/index.html index dc51fad2..846211e0 100644 --- a/web/public/fr/formats/index.html +++ b/web/public/fr/formats/index.html @@ -287,7 +287,7 @@

32 formats de fichiers, chacun généré à une taille exacte

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/hi/formats/index.html b/web/public/hi/formats/index.html index a6d4dd70..3d3697b1 100644 --- a/web/public/hi/formats/index.html +++ b/web/public/hi/formats/index.html @@ -287,7 +287,7 @@

32 फ़ाइल फ़ॉर्मैट, हर एक सटीक आ otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/id/format/index.html b/web/public/id/format/index.html index 2225ae1e..56d4e6c9 100644 --- a/web/public/id/format/index.html +++ b/web/public/id/format/index.html @@ -287,7 +287,7 @@

32 format file, masing-masing dibuat dengan ukuran tepat

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/it/formati/index.html b/web/public/it/formati/index.html index d68f00aa..3770db4b 100644 --- a/web/public/it/formati/index.html +++ b/web/public/it/formati/index.html @@ -287,7 +287,7 @@

32 formati di file, ognuno generato a una dimensione esatta

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/ja/formats/index.html b/web/public/ja/formats/index.html index cbcda979..3e928a0b 100644 --- a/web/public/ja/formats/index.html +++ b/web/public/ja/formats/index.html @@ -285,7 +285,7 @@

32種類のファイル形式、どれも正確なサイズで生成

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/ko/formats/index.html b/web/public/ko/formats/index.html index 0320d823..da85e73c 100644 --- a/web/public/ko/formats/index.html +++ b/web/public/ko/formats/index.html @@ -285,7 +285,7 @@

파일 형식 32가지, 모두 정확한 크기로 생성됩니다

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/nl/formaten/index.html b/web/public/nl/formaten/index.html index 36617965..062c1b2b 100644 --- a/web/public/nl/formaten/index.html +++ b/web/public/nl/formaten/index.html @@ -287,7 +287,7 @@

32 bestandsformaten, elk gegenereerd in een exacte grootte

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/pl/formaty/index.html b/web/public/pl/formaty/index.html index b161b2af..92624a80 100644 --- a/web/public/pl/formaty/index.html +++ b/web/public/pl/formaty/index.html @@ -286,7 +286,7 @@

32 formatów plików, każdy generowany o dokładnym rozmiarze

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/pt-br/formatos/index.html b/web/public/pt-br/formatos/index.html index f94c1f8b..933998bc 100644 --- a/web/public/pt-br/formatos/index.html +++ b/web/public/pt-br/formatos/index.html @@ -287,7 +287,7 @@

32 formatos de arquivo, cada um gerado em um tamanho exato

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/ro/formate/index.html b/web/public/ro/formate/index.html index 155c857d..0dba8530 100644 --- a/web/public/ro/formate/index.html +++ b/web/public/ro/formate/index.html @@ -287,7 +287,7 @@

32 formate de fișiere, fiecare generat la o dimensiune exactă

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/ru/formats/index.html b/web/public/ru/formats/index.html index a6f79a1d..58f1efcf 100644 --- a/web/public/ru/formats/index.html +++ b/web/public/ru/formats/index.html @@ -287,7 +287,7 @@

32 форматов файлов, каждый создаётся точно otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/th/formats/index.html b/web/public/th/formats/index.html index cefaab3f..41874ac3 100644 --- a/web/public/th/formats/index.html +++ b/web/public/th/formats/index.html @@ -286,7 +286,7 @@

32 รูปแบบไฟล์ แต่ละรูปแบบสร้ otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/tr/bicimler/index.html b/web/public/tr/bicimler/index.html index c4505883..31e83384 100644 --- a/web/public/tr/bicimler/index.html +++ b/web/public/tr/bicimler/index.html @@ -286,7 +286,7 @@

32 dosya biçimi, her biri tam boyutta üretilir

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/uk/formats/index.html b/web/public/uk/formats/index.html index 5b0c857e..d6044fef 100644 --- a/web/public/uk/formats/index.html +++ b/web/public/uk/formats/index.html @@ -287,7 +287,7 @@

32 форматів файлів, кожен створюється точн otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/vi/dinh-dang/index.html b/web/public/vi/dinh-dang/index.html index 5f1fb533..3540c772 100644 --- a/web/public/vi/dinh-dang/index.html +++ b/web/public/vi/dinh-dang/index.html @@ -286,7 +286,7 @@

32 định dạng tệp, mỗi định dạng được tạo ở kích thư otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/zh-hans/formats/index.html b/web/public/zh-hans/formats/index.html index 713aae95..afd93ba6 100644 --- a/web/public/zh-hans/formats/index.html +++ b/web/public/zh-hans/formats/index.html @@ -285,7 +285,7 @@

32 种文件格式,每一种都按精确大小生成

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/public/zh-hant/formats/index.html b/web/public/zh-hant/formats/index.html index 37f5e3e8..bc3c2faa 100644 --- a/web/public/zh-hant/formats/index.html +++ b/web/public/zh-hant/formats/index.html @@ -285,7 +285,7 @@

32 種檔案格式,每一種都以精確大小產生

otlp OpenTelemetry Protocol JSON - .jsonl + .jsonl .json 1266 full otelcol diff --git a/web/templates/partials.html b/web/templates/partials.html index 5258c450..7fe4968c 100644 --- a/web/templates/partials.html +++ b/web/templates/partials.html @@ -113,7 +113,7 @@ {{ .ID }} {{ .Name }} - {{ .Extension }} + {{ range $i, $e := .Extensions }}{{ if $i }} {{ end }}{{ $e }}{{ end }} {{ .Smallest }} {{ .Fidelity }} {{ if eq .Oracle "none" }}{{ $.Term "oracleNone" }}{{ else }}{{ .Oracle }}{{ end }} From 7d87c205fcca47defe5df4e1e493d9fe60b0b654 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 11 Nov 2026 14:06:13 +0100 Subject: [PATCH 2/3] build(deps): bump github.com/nicksnyder/go-i18n/v2 from 2.6.1 to 2.7.0 The change Dependabot proposed in #222, with the notices naming the version the window now links. go-i18n is linked by the window alone. Between 2.6.1 and 2.7.0: the CLDR v49 plural rules (Vietnamese, Galician and French move between groups), ParseMessageBytes, and fixes to the goi18n command. The window speaks English and Polish, whose rules did not change. go.yaml.in/yaml/v3 moves from 3.0.4 to 3.0.5 in go.sum only - nothing links it. Both LICENSE files are the same text as in the versions before. Co-Authored-By: Claude Opus 5.5 --- THIRD-PARTY-NOTICES.md | 2 +- go.mod | 2 +- go.sum | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) 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= From bf2376b4641eb7cf4fc432ff05ecf9f7cabc8353 Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Wed, 11 Nov 2026 14:16:14 +0100 Subject: [PATCH 3/3] tfg formats is held to the endings a setting gives, in its text and its JSON The guards on this change held the names the engine plans, and nothing held what tfg formats says about them: the card could go back to ".jsonl" alone, and --json could drop "extensions", with every guard green. A guard now reads both - the card of otlp with its line written out, and the JSON of every format whose settings give an ending against what they declare - with two mutation entries, one for each output. Co-Authored-By: Claude Opus 5.5 --- internal/guard/extensionbylayout_test.go | 54 ++++++++++++++++++++++++ internal/guard/mutation/entries.py | 14 ++++++ 2 files changed, 68 insertions(+) diff --git a/internal/guard/extensionbylayout_test.go b/internal/guard/extensionbylayout_test.go index 63fd8d0f..cb79927a 100644 --- a/internal/guard/extensionbylayout_test.go +++ b/internal/guard/extensionbylayout_test.go @@ -1,12 +1,15 @@ 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" @@ -132,6 +135,57 @@ func TestEveryFileIsNamedForTheSettingsItWasMadeWith(t *testing.T) { } } +// 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. diff --git a/internal/guard/mutation/entries.py b/internal/guard/mutation/entries.py index 99d81a97..a0c413b5 100644 --- a/internal/guard/mutation/entries.py +++ b/internal/guard/mutation/entries.py @@ -17691,4 +17691,18 @@ "", "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", + ), ]