From 2a14ce095caf8eb3c5e56b26ce2d5ac05b21905e Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Tue, 29 Sep 2026 08:41:11 +0200 Subject: [PATCH] site: a page for every preset, made from the registry, in both languages /presets/ and /pl/presety/ list the six presets, and each preset gets a page of its own: the question, what it usually catches, what the set holds at its defaults (files, targets, bytes, formats), how many files the system under test should accept, reject or decide on, every setting with what it takes and its default, and the commands to run it. The home page and the documentation link to them, and Presets is in the header. Nothing on those pages is typed. The settings come from the registry, the budget from tfg preset show and the reactions from a dry run of the set, asked in process by the guard. The pages themselves are made from the registry after the language text is filled in, so a seventh preset gets its pages without anybody adding them, and without its Polish words the render stops and names it. The English words of a preset are copies of the registry and are now held to it - the questions had been copies with nothing comparing them. The Polish page has to carry as many catches as the English one, because a page one sentence short renders without complaint. A text setting is described by its shape rather than the word "text", on the formats page as well, where two archive settings said nothing about the value they want. The sitemap guard counts the pages that were rendered rather than the ones the language files list, and asserts it reached the made pages. Co-Authored-By: Claude Opus 5.5 --- internal/guard/site_test.go | 57 ++-- internal/guard/sitepresets_test.go | 262 +++++++++++++++ internal/site/presets.go | 304 ++++++++++++++++++ internal/site/render.go | 51 ++- internal/site/site.go | 61 ++-- internal/site/view.go | 30 +- web/assets/site.css | 2 +- web/content/en/docs.html | 5 +- web/content/en/index.html | 12 + web/content/en/preset.html | 91 ++++++ web/content/en/presets.html | 30 ++ web/content/en/site.json | 127 +++++++- web/content/pl/docs.html | 4 +- web/content/pl/index.html | 12 + web/content/pl/preset.html | 91 ++++++ web/content/pl/presets.html | 31 ++ web/content/pl/site.json | 127 +++++++- web/public/404.html | 2 + web/public/assets/site.css | 2 +- web/public/create-file-exact-size/index.html | 2 + web/public/docs/index.html | 25 +- web/public/faq/index.html | 2 + web/public/formats/index.html | 8 +- web/public/index.html | 45 +++ web/public/pl/dokumentacja/index.html | 24 +- web/public/pl/faq/index.html | 2 + web/public/pl/formaty/index.html | 8 +- web/public/pl/index.html | 45 +++ .../pl/plik-o-zadanym-rozmiarze/index.html | 2 + .../pl/presety/empty-and-minimal/index.html | 200 ++++++++++++ .../pl/presety/filename-handling/index.html | 199 ++++++++++++ web/public/pl/presety/index.html | 177 ++++++++++ .../pl/presety/size-boundaries/index.html | 213 ++++++++++++ .../pl/presety/tabular-import/index.html | 207 ++++++++++++ .../pl/presety/text-encoding/index.html | 200 ++++++++++++ .../pl/presety/upload-validation/index.html | 229 +++++++++++++ web/public/pl/zastosowania/index.html | 2 + .../presets/empty-and-minimal/index.html | 200 ++++++++++++ .../presets/filename-handling/index.html | 199 ++++++++++++ web/public/presets/index.html | 176 ++++++++++ web/public/presets/size-boundaries/index.html | 213 ++++++++++++ web/public/presets/tabular-import/index.html | 207 ++++++++++++ web/public/presets/text-encoding/index.html | 200 ++++++++++++ .../presets/upload-validation/index.html | 229 +++++++++++++ web/public/sitemap.xml | 42 +++ web/public/use-cases/index.html | 2 + web/templates/layout.html | 2 +- web/templates/partials.html | 11 +- web/templates/social.html | 2 +- 49 files changed, 4264 insertions(+), 110 deletions(-) create mode 100644 internal/guard/sitepresets_test.go create mode 100644 internal/site/presets.go create mode 100644 web/content/en/preset.html create mode 100644 web/content/en/presets.html create mode 100644 web/content/pl/preset.html create mode 100644 web/content/pl/presets.html create mode 100644 web/public/pl/presety/empty-and-minimal/index.html create mode 100644 web/public/pl/presety/filename-handling/index.html create mode 100644 web/public/pl/presety/index.html create mode 100644 web/public/pl/presety/size-boundaries/index.html create mode 100644 web/public/pl/presety/tabular-import/index.html create mode 100644 web/public/pl/presety/text-encoding/index.html create mode 100644 web/public/pl/presety/upload-validation/index.html create mode 100644 web/public/presets/empty-and-minimal/index.html create mode 100644 web/public/presets/filename-handling/index.html create mode 100644 web/public/presets/index.html create mode 100644 web/public/presets/size-boundaries/index.html create mode 100644 web/public/presets/tabular-import/index.html create mode 100644 web/public/presets/text-encoding/index.html create mode 100644 web/public/presets/upload-validation/index.html diff --git a/internal/guard/site_test.go b/internal/guard/site_test.go index f2564178..ed5b126f 100644 --- a/internal/guard/site_test.go +++ b/internal/guard/site_test.go @@ -18,7 +18,6 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/cli" "github.com/donislawdev/TestingFilesGenerator/internal/format" _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" - "github.com/donislawdev/TestingFilesGenerator/internal/preset" "github.com/donislawdev/TestingFilesGenerator/internal/site" "github.com/donislawdev/TestingFilesGenerator/internal/version" ) @@ -97,14 +96,7 @@ func factsFromTheProgram(t *testing.T) site.Facts { for _, d := range format.All() { props := make([]site.Property, 0, len(d.Properties)) for _, p := range d.Properties { - props = append(props, site.Property{ - Name: p.Name, - Kind: string(p.Kind), - Min: p.Min, - Max: p.Max, - Unit: p.Unit, - Choices: append([]string(nil), p.Choices...), - }) + props = append(props, siteProperty(p)) } formats = append(formats, site.Format{ ID: d.ID, @@ -125,16 +117,11 @@ func factsFromTheProgram(t *testing.T) site.Facts { }) } - ids := make([]string, 0, len(preset.All())) - for _, p := range preset.All() { - ids = append(ids, p.ID) - } - return site.Facts{ Version: version.Version, Formats: formats, ExitCodes: exitCodesInOrder(), - Presets: ids, + Presets: presetFactsFromTheProgram(t), Commands: commandsTheToolPrints(t), Downloads: declaredDownloads(), // Fixed on purpose. See the comment on the field. @@ -396,11 +383,9 @@ func TestEveryLanguageDescribesEverythingTheProgramCanProduce(t *testing.T) { t.Errorf("exit code %d has no meaning in %s, so that row of the table would be blank", code, lang.Code) } } - for _, id := range facts.Presets { - if _, ok := lang.Presets[id]; !ok { - t.Errorf("the preset %q has no question in %s", id, lang.Code) - } - } + // Presets are asked by TestEveryPresetIsDescribedInEveryLanguage, + // which holds far more of them than a question. + // // The other direction as well, which the rows above do not ask. A // command dropped from the program leaves its summary behind in both // language files, and the page would then be a list of what the tool @@ -449,6 +434,19 @@ func TestEveryLanguageDescribesEverythingTheProgramCanProduce(t *testing.T) { if _, ok := lang.Terms[p.Kind]; !ok { t.Errorf("%s.%s is a %q and %s has no word for that kind", f.ID, p.Name, p.Kind, lang.Code) } + // The shape is what the page shows instead of the kind, so + // its words are asked for the same way. In English they are + // the registry's own, since the page and the program + // describe one setting. + if p.Shape != "" { + said, ok := lang.Terms[p.Shape] + if !ok { + t.Errorf("%s.%s takes %q and %s has no words for that", f.ID, p.Name, p.Shape, lang.Code) + } + if ok && lang.Code == "en" && said != p.Shape { + t.Errorf("the registry says %s.%s takes %q and the English page says %q", f.ID, p.Name, p.Shape, said) + } + } if p.Unit != "" { if _, ok := lang.Terms[p.Unit]; !ok { t.Errorf("%s.%s counts %q and %s has no word for it, so the page would read as half translated", f.ID, p.Name, p.Unit, lang.Code) @@ -666,9 +664,24 @@ func TestTheSitemapNeedsNoSchemaButItsOwn(t *testing.T) { } } - pages := 0 + // Counted from what was rendered rather than from the language files. + // Since 2026-09-29 a page per preset is made from the registry, and those + // pages are in no language file - counting the files would compare the + // sitemap with a set that is missing them. So the count is every page + // published, and it is held to having reached the made pages at all: + // a count that silently stopped seeing them would agree with a sitemap + // that had also stopped naming them. + pages, written := 0, 0 + for path := range rendered { + if filepath.Base(path) == "index.html" { + pages++ + } + } for _, language := range s.Languages { - pages += len(language.Pages) + written += len(language.Pages) + } + if len(s.Facts.Presets) > 0 && pages <= written { + t.Fatalf("the site renders %d pages and its language files list %d, so the preset pages were not counted", pages, written) } if locations != pages { t.Errorf("the site has %d pages and the sitemap names %d of them, so a crawler reading it is told about the wrong set", pages, locations) diff --git a/internal/guard/sitepresets_test.go b/internal/guard/sitepresets_test.go new file mode 100644 index 00000000..5a89684b --- /dev/null +++ b/internal/guard/sitepresets_test.go @@ -0,0 +1,262 @@ +package guard + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "slices" + "sort" + "sync" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/cli" + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/manifest" + "github.com/donislawdev/TestingFilesGenerator/internal/preset" + "github.com/donislawdev/TestingFilesGenerator/internal/site" +) + +// Every preset has a page of its own on the site since 2026-09-29, and what a +// page says about its preset is asked of the program here rather than written +// down beside it: the settings from the registry, what the set costs from +// tfg preset show, and how many files the system under test should take or +// turn away from a dry run of the set itself. The reasoning is in +// docs/PRESET-PAGES-2026-09-29.md. + +// presetAnswers is asked once per test binary. Every site guard renders the +// site, and asking six presets for a dry run each time would repeat the same +// second of work for the same answer. +var presetAnswers struct { + once sync.Once + facts []site.PresetFacts + err error +} + +// presetFactsFromTheProgram is every registered preset as the site shows it. +func presetFactsFromTheProgram(t *testing.T) []site.PresetFacts { + t.Helper() + presetAnswers.once.Do(func() { + presetAnswers.facts, presetAnswers.err = askEveryPreset() + }) + if presetAnswers.err != nil { + t.Fatalf("asking the program about its presets: %v", presetAnswers.err) + } + return presetAnswers.facts +} + +func askEveryPreset() ([]site.PresetFacts, error) { + // A dry run writes nothing, and it is still given a directory of its own + // to not write into, removed afterwards, in case that ever changes. + scratch, err := os.MkdirTemp("", "tfg-site-presets-") + if err != nil { + return nil, err + } + defer func() { _ = os.RemoveAll(scratch) }() + + out := make([]site.PresetFacts, 0, len(preset.All())) + for _, p := range preset.All() { + facts := site.PresetFacts{ID: p.ID} + for _, param := range p.Parameters { + facts.Settings = append(facts.Settings, site.Setting{ + Property: siteProperty(param), + Default: param.Default, + Placeholder: p.SaidWhenDefaulted[param.Name] != "", + }) + } + for _, name := range p.Reads { + facts.Reads = append(facts.Reads, site.Read{Name: name, Default: p.ReadDefaults[name]}) + } + if facts.Budget, err = budgetOf(p.ID); err != nil { + return nil, err + } + if facts.Outcomes, err = outcomesOf(p.ID, filepath.Join(scratch, p.ID)); err != nil { + return nil, err + } + out = append(out, facts) + } + return out, nil +} + +// siteProperty is one setting flattened for the site, the same way for a +// format and for a preset - they are one type in the program, so the site +// describes them with one function. +func siteProperty(p format.Property) site.Property { + return site.Property{ + Name: p.Name, + Kind: string(p.Kind), + Min: p.Min, + Max: p.Max, + Unit: p.Unit, + Choices: append([]string(nil), p.Choices...), + Shape: p.Shape, + } +} + +// runQuietly runs one command in this process and hands back what it printed. +func runQuietly(args ...string) ([]byte, error) { + var stdout, stderr bytes.Buffer + if code := cli.Run(context.Background(), args, &stdout, &stderr); code != cli.ExitOK { + return nil, fmt.Errorf("tfg %v ended with %d:\n%s", args, code, stderr.String()) + } + return stdout.Bytes(), nil +} + +// budgetOf is what tfg preset show says a preset costs at its defaults. +func budgetOf(id string) (site.Budget, error) { + printed, err := runQuietly("preset", "show", id, "--json") + if err != nil { + return site.Budget{}, err + } + var shown struct { + Budget *struct { + Targets int `json:"targets"` + Files int `json:"files"` + TotalBytes int64 `json:"total_bytes"` + Formats []string `json:"formats"` + } `json:"budget"` + } + if err := json.Unmarshal(printed, &shown); err != nil { + return site.Budget{}, fmt.Errorf("reading what tfg preset show %s printed: %w", id, err) + } + // Refused rather than read as zero. A page announcing a set of no files + // because a field was renamed is the quiet kind of wrong this file is for. + if shown.Budget == nil || shown.Budget.Files == 0 { + return site.Budget{}, fmt.Errorf("tfg preset show %s --json printed no budget, so its page would say the set is empty", id) + } + b := shown.Budget + return site.Budget{Targets: b.Targets, Files: b.Files, Bytes: b.TotalBytes, Formats: b.Formats}, nil +} + +// outcomesOf counts the reactions a dry run of the preset declares, by name. +func outcomesOf(id, dir string) ([]site.Outcome, error) { + printed, err := runQuietly("generate", "--preset", id, "--dry-run", "--json", "--out", dir) + if err != nil { + return nil, err + } + var run struct { + Files []struct { + Expected struct { + Outcome string `json:"outcome"` + } `json:"expected"` + } `json:"files"` + } + if err := json.Unmarshal(printed, &run); err != nil { + return nil, fmt.Errorf("reading the dry run of %s: %w", id, err) + } + counts := map[string]int{} + for _, f := range run.Files { + counts[f.Expected.Outcome]++ + } + if len(counts) == 0 { + return nil, fmt.Errorf("a dry run of %s declared no files, so its page would have no reactions to show", id) + } + out := make([]site.Outcome, 0, len(counts)) + for name, n := range counts { + out = append(out, site.Outcome{Name: name, Count: n}) + } + sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name }) + return out, nil +} + +// TestEveryPresetIsDescribedInEveryLanguage asks for the words of every +// preset page, in both directions. +// +// The render already stops on a word that is missing, so this is about the +// two things it cannot see. The English words are copies of the registry, and +// a copy nobody compares is the defect this whole site exists to prevent: the +// questions were copies for a month before this and nothing held them. And the +// number of sentences - a Polish page with one catch fewer renders without a +// complaint and says less than the English one. +func TestEveryPresetIsDescribedInEveryLanguage(t *testing.T) { + langs := languagesOnDisk(t) + sawEnglish := false + for _, lang := range langs { + for _, p := range preset.All() { + text, ok := lang.Presets[p.ID] + if !ok { + t.Errorf("the preset %q has no words in %s", p.ID, lang.Code) + continue + } + describesItsPreset(t, lang, p, text) + } + for id := range lang.Presets { + if _, err := preset.Get(id); err != nil { + t.Errorf("%s describes a preset %q that the program does not register", lang.Code, id) + } + } + sawEnglish = sawEnglish || lang.Code == "en" + } + if !sawEnglish { + t.Fatal("no English language file was read, so nothing was compared with the registry") + } +} + +func describesItsPreset(t *testing.T, lang site.Language, p preset.Preset, text site.PresetText) { + t.Helper() + if text.Question == "" || text.Title == "" || text.PageTitle == "" || text.Description == "" { + t.Errorf("the preset %q is missing a question, title, page title or description in %s", p.ID, lang.Code) + } + if len(text.Catches) != len(p.Catches) { + t.Errorf("the registry says %d things %q catches and %s says %d", len(p.Catches), p.ID, lang.Code, len(text.Catches)) + } + declared := map[string]bool{} + for _, param := range p.Parameters { + declared[param.Name] = true + if _, ok := text.Details[param.Name]; !ok { + t.Errorf("--%s of %q has no sentence in %s", param.Name, p.ID, lang.Code) + } + if param.Shape != "" { + said, ok := lang.Terms[param.Shape] + if !ok { + t.Errorf("--%s of %q takes %q and %s has no words for that", param.Name, p.ID, param.Shape, lang.Code) + } + if ok && lang.Code == "en" && said != param.Shape { + t.Errorf("the registry says --%s of %q takes %q and the English page says %q", param.Name, p.ID, param.Shape, said) + } + } + } + for name := range text.Details { + if !declared[name] { + t.Errorf("%s describes a setting --%s that %q does not declare", lang.Code, name, p.ID) + } + } + if lang.Code != "en" { + return + } + if text.Question != p.Question || text.Title != p.Title { + t.Errorf("the registry calls %q %q and asks %q, and the English page says %q and %q", + p.ID, p.Title, p.Question, text.Title, text.Question) + } + if !slices.Equal(text.Catches, p.Catches) { + t.Errorf("the English page lists what %q catches differently from the registry:\n page: %q\n registry: %q", + p.ID, text.Catches, p.Catches) + } + for _, param := range p.Parameters { + if said := text.Details[param.Name]; said != param.Detail { + t.Errorf("the registry describes --%s of %q as %q and the English page says %q", param.Name, p.ID, param.Detail, said) + } + } +} + +// TestEveryReactionHasItsMeaningInEveryLanguage asks for the words of every +// outcome a manifest can declare, not only the ones today's presets happen to +// produce. A preset that starts declaring sanitize would otherwise stop the +// render on the day it lands, in a change about something else. +func TestEveryReactionHasItsMeaningInEveryLanguage(t *testing.T) { + outcomes := []string{manifest.OutcomeAccept, manifest.OutcomeReject, manifest.OutcomeSanitize, manifest.OutcomeUnspecified} + for _, lang := range languagesOnDisk(t) { + for _, o := range outcomes { + if lang.Outcomes[o] == "" { + t.Errorf("the outcome %q has no meaning in %s", o, lang.Code) + } + } + for o := range lang.Outcomes { + if !slices.Contains(outcomes, o) { + t.Errorf("%s explains an outcome %q that no manifest declares", lang.Code, o) + } + } + } +} diff --git a/internal/site/presets.go b/internal/site/presets.go new file mode 100644 index 00000000..704688b1 --- /dev/null +++ b/internal/site/presets.go @@ -0,0 +1,304 @@ +// This file gives every preset the program registers a page of its own, in +// every language, and holds what one of those pages is rendered against. +// +// These are the first pages of the site that come from a list rather than +// from site.json. The list is the registry, so a seventh preset gets its pages +// without anybody adding them - and without anybody writing its words, the +// render stops and names the preset and the language, which is the whole point +// of making the pages here rather than by hand. The reasoning, and what was +// left out on purpose, is in docs/PRESET-PAGES-2026-09-29.md. + +package site + +import ( + "fmt" + "path" + "regexp" + "strconv" + "strings" +) + +// PresetFacts is one preset as the program describes it. +// +// Everything here is a name or a number. The sentences about a preset differ +// by language and live in PresetText, for the same reason a unit does. +type PresetFacts struct { + ID string + Settings []Setting + Reads []Read + Budget Budget + Outcomes []Outcome +} + +// Setting is one parameter a preset declares. +type Setting struct { + Property + Default string + // Placeholder marks a default that stands in for a number only the + // system under test knows, such as the limit of an upload form. The + // program says so out loud when that default is used, and the page says + // it beside the value, so nobody copies our number and believes it is + // theirs. + Placeholder bool +} + +// Read is a flag of the tool itself that a preset gives a default to, rather +// than declaring a parameter of the same meaning. +type Read struct { + Name string + Default string +} + +// Budget is what the set costs at its defaults, as tfg preset show prints it. +type Budget struct { + Targets int + Files int + Bytes int64 + Formats []string +} + +// Outcome is how many files of the set a system should meet with one +// reaction, as the manifest of a dry run declares them. +type Outcome struct { + Name string + Count int +} + +// PresetText is every word one preset needs, in one language. +// +// Title is what the preset is called and PageTitle is the title of its page +// for a search engine, which has to say more than a name in the same few +// words. Details is keyed by the parameter name. Catches and Details in +// English are copies of the registry, held to it by +// TestEveryLanguageDescribesEverythingTheProgramCanProduce. +type PresetText struct { + Question string `json:"question"` + Title string `json:"title"` + PageTitle string `json:"pageTitle"` + Description string `json:"description"` + Catches []string `json:"catches"` + Details map[string]string `json:"details"` +} + +// presetParent is the key of the page every preset page sits under. +const presetParent = "presets" + +// presetKey pairs the pages of one preset across languages. +func presetKey(id string) string { return "preset/" + id } + +// addressable is what an id has to look like to become part of an address. +// +// The registry refuses only an empty id and a repeated one, and an id is +// written into a path on disk and a URL here. So this is the one place that +// asks, rather than trusting a rule nothing else holds. +var addressable = regexp.MustCompile(`^[a-z0-9]+(-[a-z0-9]+)*$`) + +// withPresetPages adds one page per preset to a language already filled in +// with the facts. +func (l Language) withPresetPages(f Facts) (Language, error) { + if len(f.Presets) == 0 { + return l, nil + } + parent, ok := find(l, presetParent) + if !ok { + return l, fmt.Errorf("the %s pages have no %q page, and every preset page sits under it", l.Code, presetParent) + } + pages := append([]Page(nil), l.Pages...) + for _, p := range f.Presets { + if !addressable.MatchString(p.ID) { + return l, fmt.Errorf("the preset %q cannot be part of an address - an id of lower case letters, digits and single dashes can", p.ID) + } + text, ok := l.Presets[p.ID] + if !ok || text.Title == "" || text.PageTitle == "" || text.Description == "" { + return l, fmt.Errorf("the preset %q has no title, page title or description written in %s", p.ID, l.Code) + } + pages = append(pages, Page{ + Key: presetKey(p.ID), + Slug: path.Join(parent.Slug, p.ID), + Title: text.PageTitle, + Description: text.Description, + Nav: text.Title, + Parent: presetParent, + Template: "preset", + Item: p.ID, + }) + } + l.Pages = pages + return l, nil +} + +// expanded runs every word of one preset through the facts. +func (t PresetText) expanded(through func(string) string) PresetText { + out := t + out.Question = through(t.Question) + out.Title = through(t.Title) + out.PageTitle = through(t.PageTitle) + out.Description = through(t.Description) + out.Catches = make([]string, len(t.Catches)) + for i, c := range t.Catches { + out.Catches[i] = through(c) + } + if t.Details != nil { + out.Details = make(map[string]string, len(t.Details)) + for k, v := range t.Details { + out.Details[k] = through(v) + } + } + return out +} + +// PresetList is every preset this build registers, described in the language +// being rendered, each with the address of its own page. +func (v view) PresetList() ([]Preset, error) { + out := make([]Preset, 0, len(v.Facts.Presets)) + for _, p := range v.Facts.Presets { + text, ok := v.Lang.Presets[p.ID] + if !ok || text.Question == "" { + return nil, fmt.Errorf("the preset %q has no question written in %s", p.ID, v.Lang.Code) + } + page, ok := find(v.Lang, presetKey(p.ID)) + if !ok { + return nil, fmt.Errorf("the preset %q has no %s page to link to", p.ID, v.Lang.Code) + } + out = append(out, Preset{ID: p.ID, Title: text.Title, Question: text.Question, URL: pageURL(v.Lang, page)}) + } + return out, nil +} + +// PresetPage is what the page of one preset shows. +type PresetPage struct { + ID string + Title string + Question string + Catches []string + Settings []SettingRow + Budget Budget + // Bytes is the total of the budget grouped in threes, the way tfg preset + // show prints it, so the two can be read side by side. + Bytes string + Outcomes []OutcomeRow +} + +// SettingRow is one line of the settings table. +type SettingRow struct { + Flag string + Takes string + Default string + Detail string + Placeholder bool +} + +// OutcomeRow is one line of the reactions table. +type OutcomeRow struct { + Name string + Meaning string + Count int +} + +// Placeholders are the settings whose default is ours rather than the +// reader's, which is what a command on the page has to spell out. +func (p PresetPage) Placeholders() []SettingRow { + var out []SettingRow + for _, s := range p.Settings { + if s.Placeholder { + out = append(out, s) + } + } + return out +} + +// Preset is the preset the page being rendered is about. +func (v view) Preset() (PresetPage, error) { + id := v.Page.Item + var facts *PresetFacts + for i := range v.Facts.Presets { + if v.Facts.Presets[i].ID == id { + facts = &v.Facts.Presets[i] + } + } + text, ok := v.Lang.Presets[id] + if facts == nil || !ok { + return PresetPage{}, fmt.Errorf("the %s page %q is about a preset %q that the program or the language does not know", v.Lang.Code, v.Page.Key, id) + } + settings, err := v.settingRows(*facts, text) + if err != nil { + return PresetPage{}, err + } + outcomes, err := v.outcomeRows(*facts) + if err != nil { + return PresetPage{}, err + } + return PresetPage{ + ID: id, + Title: text.Title, + Question: text.Question, + Catches: text.Catches, + Settings: settings, + Budget: facts.Budget, + Bytes: grouped(facts.Budget.Bytes), + Outcomes: outcomes, + }, nil +} + +// settingRows describes every setting of a preset, its own parameters first +// and then the flags of the tool it gives a default to. +// +// A flag of the tool has no sentence in the registry of presets, because it is +// the tool's rather than the preset's. So its words come from the word list, +// keyed by the flag, and a preset that starts reading a second flag stops the +// render until somebody writes them. +func (v view) settingRows(f PresetFacts, text PresetText) ([]SettingRow, error) { + out := make([]SettingRow, 0, len(f.Settings)+len(f.Reads)) + for _, s := range f.Settings { + takes, err := v.AllowedOf(s.Property) + if err != nil { + return nil, err + } + detail, ok := text.Details[s.Name] + if !ok { + return nil, fmt.Errorf("the setting --%s of the preset %q has no sentence written in %s", s.Name, f.ID, v.Lang.Code) + } + out = append(out, SettingRow{Flag: s.Name, Takes: takes, Default: s.Default, Detail: detail, Placeholder: s.Placeholder}) + } + for _, r := range f.Reads { + takes, err := v.Word("readTakes." + r.Name) + if err != nil { + return nil, err + } + detail, err := v.Word("read." + r.Name) + if err != nil { + return nil, err + } + out = append(out, SettingRow{Flag: r.Name, Takes: takes, Default: r.Default, Detail: detail}) + } + return out, nil +} + +// outcomeRows says what each reaction in the set means, in the language being +// rendered. +func (v view) outcomeRows(f PresetFacts) ([]OutcomeRow, error) { + out := make([]OutcomeRow, 0, len(f.Outcomes)) + for _, o := range f.Outcomes { + meaning, ok := v.Lang.Outcomes[o.Name] + if !ok { + return nil, fmt.Errorf("the outcome %q has no meaning written in %s", o.Name, v.Lang.Code) + } + out = append(out, OutcomeRow{Name: o.Name, Meaning: meaning, Count: o.Count}) + } + return out, nil +} + +// grouped writes a byte count in threes separated by spaces, as the command +// line does. A plain space rather than a narrow one, because the English +// pages are held to ASCII. +func grouped(n int64) string { + digits := strconv.FormatInt(n, 10) + var b strings.Builder + for i, d := range digits { + if i > 0 && (len(digits)-i)%3 == 0 { + b.WriteByte(' ') + } + b.WriteRune(d) + } + return b.String() +} diff --git a/internal/site/render.go b/internal/site/render.go index cb2fbfb9..c6262919 100644 --- a/internal/site/render.go +++ b/internal/site/render.go @@ -110,11 +110,37 @@ func (s Site) filledLanguages() ([]Language, error) { if err != nil { return nil, err } + // After expanding, so the page of a preset takes its title from words + // that are already filled in, and before anything is rendered, so the + // sitemap, the language links and the links between pages all see it. + if done, err = done.withPresetPages(s.Facts); err != nil { + return nil, err + } out[i] = done } return out, nil } +// navFor is the header as seen from one page. +// +// A page made from a list is not in the header, and there would be one link +// per preset in it if it were. The page it sits under is marked instead. +func navFor(lang Language, page Page) []NavItem { + var out []NavItem + for _, item := range lang.Pages { + if item.Parent != "" { + continue + } + out = append(out, NavItem{ + Label: item.Nav, + URL: pageURL(lang, item), + Current: item.Key == page.Key, + Section: page.Parent != "" && item.Key == page.Parent, + }) + } + return out +} + // copyExtras publishes files that live elsewhere in the repository. // // The window screenshot and the application icon come this way rather than @@ -141,13 +167,14 @@ func (s Site) viewFor(lang Language, page Page) (view, error) { Path: pageURL(lang, page), Canonical: s.Origin() + pageURL(lang, page), IsHome: page.Slug == "", + Nav: navFor(lang, page), } - for _, item := range lang.Pages { - v.Nav = append(v.Nav, NavItem{ - Label: item.Nav, - URL: pageURL(lang, item), - Current: item.Key == page.Key, - }) + if page.Parent != "" { + up, ok := find(lang, page.Parent) + if !ok { + return view{}, fmt.Errorf("the %s page %q sits under %q, which is not there", lang.Code, page.Key, page.Parent) + } + v.Up = &NavItem{Label: up.Nav, URL: pageURL(lang, up)} } for _, other := range s.Languages { mate, ok := find(other, page.Key) @@ -190,9 +217,7 @@ func (s Site) notFound(partials, shell string) ([]byte, error) { W: root.Words, Path: "/404.html", IsHome: false, - } - for _, item := range root.Pages { - v.Nav = append(v.Nav, NavItem{Label: item.Nav, URL: pageURL(root, item)}) + Nav: navFor(root, page), } const body = `

{{ .Word "notFoundTitle" }}

` + `

{{ .Word "notFoundLead" }}

` + @@ -223,7 +248,13 @@ func (s Site) renderPages(out map[string][]byte, partials, shell string) error { if err != nil { return err } - fragment, err := os.ReadFile(filepath.Join(s.ContentDir, lang.Code, page.Key+".html")) + // A page made from a list shares one content file with the rest + // of its list, and names it. Every other page has its own. + name := page.Key + if page.Template != "" { + name = page.Template + } + fragment, err := os.ReadFile(filepath.Join(s.ContentDir, lang.Code, name+".html")) if err != nil { return fmt.Errorf("reading the %s text of the %s page: %w", lang.Code, page.Key, err) } diff --git a/internal/site/site.go b/internal/site/site.go index 23b2a042..16adab40 100644 --- a/internal/site/site.go +++ b/internal/site/site.go @@ -40,6 +40,11 @@ type Property struct { Max int64 Unit string Choices []string + // Shape is what free text has to look like, as the registry words it. + // It is looked up among the terms like a unit, because the registry + // states it in English. Without it a text setting is described as "text", + // which says nothing about the value it wants. + Shape string } // Format is one entry of the registry, flattened for display. @@ -72,14 +77,17 @@ type Ending struct { Meaning string } -// Preset is one ready made set of files, named by the question it answers. +// Preset is one ready-made set of files, named by the question it answers, +// as a card that leads to its own page. // -// The identifier comes from the registry and the question from the language +// The identifier comes from the registry and the words from the language // file, for the same reason the units do: the registry states its question in // English, and a Polish page carrying it would be a page translated halfway. type Preset struct { ID string + Title string Question string + URL string } // Command is one command the tool offers, described in the language being @@ -123,7 +131,7 @@ type Facts struct { Version string Formats []Format ExitCodes []int - Presets []string + Presets []PresetFacts Downloads []Download // Commands is what tfg --help prints, in the order it prints it, read out @@ -196,6 +204,15 @@ type Page struct { Title string `json:"title"` Description string `json:"description"` Nav string `json:"nav"` + + // The three below are never written in site.json. They are set on the + // pages made from a list rather than by hand - one per preset - and say + // which page they sit under, which content file holds their text, and + // which item of the list they are about. A page with a parent is left out + // of the header, and the parent is marked there while it is open. + Parent string `json:"-"` + Template string `json:"-"` + Item string `json:"-"` } // Language is one whole version of the site. @@ -203,22 +220,24 @@ type Page struct { // Dir is the path prefix. It is empty for the language served at the root, // which is the one search engines are pointed at by x-default. // -// Endings, Terms, Presets and Commands are the places where a word has to -// exist for every value the program can produce, and a missing one is an error -// rather than a gap left in English. Endings is keyed by the exit code written -// out in decimal, Terms by the kind or unit exactly as the registry spells it, -// Presets by the identifier, and Commands by the name tfg --help prints. +// Endings, Terms, Presets, Commands and Outcomes are the places where a word +// has to exist for every value the program can produce, and a missing one is +// an error rather than a gap left in English. Endings is keyed by the exit code +// written out in decimal, Terms by the kind, unit or shape exactly as the +// registry spells it, Presets by the identifier, Commands by the name +// tfg --help prints, and Outcomes by the reaction a manifest declares. type Language struct { - Code string `json:"code"` - Name string `json:"name"` - Dir string `json:"dir"` - Words map[string]string `json:"words"` - Endings map[string]string `json:"endings"` - Terms map[string]string `json:"terms"` - Presets map[string]string `json:"presets"` - Commands map[string]string `json:"commands"` - Pages []Page `json:"pages"` - Faq []QA `json:"faq"` + Code string `json:"code"` + Name string `json:"name"` + Dir string `json:"dir"` + Words map[string]string `json:"words"` + Endings map[string]string `json:"endings"` + Terms map[string]string `json:"terms"` + Presets map[string]PresetText `json:"presets"` + Commands map[string]string `json:"commands"` + Outcomes map[string]string `json:"outcomes"` + Pages []Page `json:"pages"` + Faq []QA `json:"faq"` } // Site is everything needed to render. @@ -247,10 +266,16 @@ type Alternate struct { } // NavItem is one link in the header. +// +// Current is the page being read. Section is the page it sits under, which is +// marked as well so a reader on the page of one preset can see where they +// are - but told apart, because a screen reader announces "current page" for +// the first and would be wrong about the second. type NavItem struct { Label string URL string Current bool + Section bool } // Switch is the link to this page in another language. diff --git a/internal/site/view.go b/internal/site/view.go index 0b0215f4..d4fb75ec 100644 --- a/internal/site/view.go +++ b/internal/site/view.go @@ -68,8 +68,14 @@ func (l Language) expand(f Facts) (Language, error) { out.Words = everyValue(l.Words) out.Endings = everyValue(l.Endings) out.Terms = everyValue(l.Terms) - out.Presets = everyValue(l.Presets) out.Commands = everyValue(l.Commands) + out.Outcomes = everyValue(l.Outcomes) + if l.Presets != nil { + out.Presets = make(map[string]PresetText, len(l.Presets)) + for id, text := range l.Presets { + out.Presets[id] = text.expanded(through) + } + } out.Pages = make([]Page, len(l.Pages)) for i, p := range l.Pages { @@ -101,6 +107,9 @@ type view struct { Path string Body template.HTML IsHome bool + // Up is the page this one sits under, for the link back to it and the + // middle step of the breadcrumb. Nil for a page in the header. + Up *NavItem } // Word looks up a piece of interface text. @@ -137,20 +146,6 @@ func (v view) Endings() ([]Ending, error) { return out, nil } -// PresetList is every preset this build registers, described in the language -// being rendered. -func (v view) PresetList() ([]Preset, error) { - out := make([]Preset, 0, len(v.Facts.Presets)) - for _, id := range v.Facts.Presets { - question, ok := v.Lang.Presets[id] - if !ok { - return nil, fmt.Errorf("the preset %q has no question written in %s", id, v.Lang.Code) - } - out = append(out, Preset{ID: id, Question: question}) - } - return out, nil -} - // CommandList is every command tfg --help prints, in that order, summarised in // the language being rendered. // @@ -203,6 +198,11 @@ func (v view) AllowedOf(p Property) (string, error) { } return span + " " + unit, nil default: + // A shape says what free text has to look like, which the kind + // alone does not - "text" is no description of a list of sizes. + if p.Shape != "" { + return v.Term(p.Shape) + } return v.Term(p.Kind) } } diff --git a/web/assets/site.css b/web/assets/site.css index 3e009b64..c60dcaac 100644 --- a/web/assets/site.css +++ b/web/assets/site.css @@ -150,7 +150,7 @@ body { background: var(--surface-2); } -.mainnav a[aria-current="page"] { +.mainnav a[aria-current] { color: var(--text); background: var(--surface-2); } diff --git a/web/content/en/docs.html b/web/content/en/docs.html index ee60a4c6..30b6c027 100644 --- a/web/content/en/docs.html +++ b/web/content/en/docs.html @@ -217,9 +217,10 @@

What is in the manifest?

What is a preset?

- A ready made set of files that answers a common testing question, so you do not have to design the + A ready-made set of files that answers a common testing question, so you do not have to design the set yourself. Presets are ordinary recipes underneath, and eject prints the recipe so - you can edit it from there. + you can edit it from there. Each preset has a page of its own with what it + usually finds, what is in the set and every setting it takes.

{{ template "presetsList" . }}
tfg preset list
diff --git a/web/content/en/index.html b/web/content/en/index.html
index 52d613f9..ad991271 100644
--- a/web/content/en/index.html
+++ b/web/content/en/index.html
@@ -98,6 +98,18 @@ 

Other generators stop at the bytes. This one answers what your test actually

+
+

Presets

+

Pick the question, get the whole set

+

+ A preset is a set of test files designed around one testing question, so you do not have to work + out which files prove what. Each one has a page saying what it usually finds, what is in the set + and every setting it takes. +

+ {{ template "presetsList" . }} +

All presets, and how they relate to recipes

+
+

Quick start

Three commands to see it working

diff --git a/web/content/en/preset.html b/web/content/en/preset.html new file mode 100644 index 00000000..2ed60a90 --- /dev/null +++ b/web/content/en/preset.html @@ -0,0 +1,91 @@ +{{ with .Preset }} +

{{ $.Up.Label }}

+

{{ .Title }}

+

{{ .Question }}

+

+ The {{ .ID }} preset builds a whole set of real test files for this question in one + command, and a manifest.json beside them saying how your system should react to each + file. Everything below is read from the program, at the defaults of this version. +

+ +{{ if .Catches }} +
+

What does it usually find?

+
    + {{- range .Catches }} +
  • {{ . }}
  • + {{- end }} +
+
+{{ end }} + +
+

What is in the set?

+

At its defaults, as tfg preset show {{ .ID }} reports it:

+
+ + + + + + + +
Files{{ .Budget.Files }}
Targets in its recipe{{ .Budget.Targets }}
Total size{{ .Bytes }} B
Formats{{ join .Budget.Formats ", " }}
+
+

And what the manifest of that set expects from your system:

+
+ + + + + + {{- range .Outcomes }} + + {{- end }} + +
ExpectedMeaningFiles
{{ .Name }}{{ .Meaning }}{{ .Count }}
+
+
+ +
+

What can you change?

+ {{- if .Settings }} +
+ + + + + + {{- range .Settings }} + + + + + + + {{- end }} + +
SettingTakesDefaultWhat it does
--{{ .Flag }}{{ .Takes }}{{ .Default }}{{ .Detail }}{{ if .Placeholder }} This default is our placeholder, not your system's value. Pass your own.{{ end }}
+
+ {{- else }} +

This preset has no settings. The set is the same every time.

+ {{- end }} +
+ +
+

How do you run it?

+

See what the set would cost, build it, or take its recipe to edit:

+
tfg preset show {{ .ID }}
+tfg generate --preset {{ .ID }}{{ range .Placeholders }} --{{ .Flag }} {{ .Default }}{{ end }} --out ./{{ .ID }}
+tfg preset eject {{ .ID }} > {{ .ID }}.yaml
+

Or build on it in a recipe of your own, next to your tests:

+
version: 1
+extends: preset:{{ .ID }}
+{{- with .Placeholders }}
+with:
+{{- range . }}
+  {{ .Flag }}: {{ .Default }}
+{{- end }}
+{{- end }}
+
+{{ end }} diff --git a/web/content/en/presets.html b/web/content/en/presets.html new file mode 100644 index 00000000..5ebf9dfc --- /dev/null +++ b/web/content/en/presets.html @@ -0,0 +1,30 @@ +

Test file presets, one set for each testing question

+

+ A preset is a whole set of test files designed around one question, with a manifest saying how your + system should react to each file. You pick the question, the tool builds the set. Each preset has + its own page with what it usually finds, what is in the set and every setting it takes. +

+ +{{ template "presetsList" . }} + +
+

How is a preset different from a recipe?

+

+ Underneath, it is not. A preset is a recipe the tool writes for you from a few settings. + tfg preset eject prints that recipe so you can keep it next to your tests and edit it, + and a recipe of your own can build on a preset with one line, extends: preset: + followed by its id. +

+
tfg preset list
+tfg preset show size-boundaries
+tfg preset eject size-boundaries > my.yaml
+
+ +
+

Can I trust the defaults?

+

+ For the files, yes. For a number only your system knows, such as the limit of an upload form, a + default is a placeholder of ours, and the tool says so every time it uses one. The page of each + preset marks those settings, and tfg preset show says it before anything is written. +

+
diff --git a/web/content/en/site.json b/web/content/en/site.json index 3e52c382..8239b663 100644 --- a/web/content/en/site.json +++ b/web/content/en/site.json @@ -17,6 +17,13 @@ "title": "{{ .Facts.FormatCount }} Supported File Formats - PDF, DOCX, PNG, ZIP and More", "description": "Every file format this generator produces, the smallest file each one can be, and the settings each one accepts. All {{ .Facts.FormatCount }} open in the software that owns them." }, + { + "key": "presets", + "slug": "presets", + "nav": "Presets", + "title": "Test File Presets - Ready-Made Sets for QA Questions", + "description": "Ready-made sets of test files, each answering one testing question: upload limits, file names, encodings, table imports, empty files and upload validation." + }, { "key": "docs", "slug": "docs", @@ -80,7 +87,9 @@ "footerPrivacy": "This site loads no fonts, no scripts and no trackers from anywhere. It sets no cookies.", "notFoundTitle": "That page is not here", "notFoundLead": "The address you followed does not match any page on this site.", - "notFoundBack": "Go to the home page" + "notFoundBack": "Go to the home page", + "read.format": "The format of every file in the set. It is a flag of the tool itself, and the preset only gives it a default.", + "readTakes.format": "a format id from the formats page" }, "endings": { "0": "Everything worked.", @@ -96,12 +105,101 @@ "143": "Stopped by a signal, which is what a CI timeout looks like." }, "presets": { - "empty-and-minimal": "Does a file that is valid and as small as the format allows get through?", - "filename-handling": "Will my system store, show and give back a file name it did not expect?", - "size-boundaries": "Is a size limit enforced exactly where it is declared?", - "tabular-import": "Does my table import survive what real tools export?", - "text-encoding": "Does my reader know which encoding a file is in, or is it guessing?", - "upload-validation": "Does my upload form take what it should and turn the rest away?" + "empty-and-minimal": { + "question": "Does a file that is valid and as small as the format allows get through?", + "title": "Empty and minimal", + "pageTitle": "Smallest Valid and Empty Test Files in Every Format", + "description": "The smallest valid file this tool writes in each of its {{ .Facts.FormatCount }} formats, plus an empty file where the format allows one, each with the reaction to expect.", + "catches": [ + "a valid file turned away for being too small, where the check counts bytes instead of reading them", + "an empty file that brings the reader down rather than being reported", + "a picture one pixel wide that divides by zero on the way to a thumbnail", + "storage that reads nought bytes as a failed upload and keeps retrying" + ], + "details": { + "formats": "Which formats the set is built from. Leave it at all for every format this build has, or name the ones your system accepts." + } + }, + "filename-handling": { + "question": "Will my system store, show and give back a file name it did not expect?", + "title": "File name handling", + "pageTitle": "Problematic File Names for Testing - Unicode and Length", + "description": "Test files whose names break uploads and storage: other scripts and emoji, a right to left override, invisible characters, shell and SQL syntax, length limits.", + "catches": [ + "a name that looks like a different one on screen, in a log or in a list", + "a name cut, trimmed or rewritten between upload and storage", + "a length limit counted in characters where the storage counts bytes" + ], + "details": {} + }, + "size-boundaries": { + "question": "Is a size limit enforced exactly where it is declared?", + "title": "Size boundaries", + "pageTitle": "Test an Upload Size Limit - Files at the Exact Boundary", + "description": "Files one byte under, at and one byte over the size limit your system declares, plus wider steps either side, each marked with whether it should be accepted.", + "catches": [ + "off by one errors at the limit", + "MB confused with MiB, which is 4.8 per cent and enough to let a file through that should not pass", + "a limit enforced in the browser and not on the server" + ], + "details": { + "limit": "The size limit your system declares. Everything else is measured from it.", + "spread": "How far either side of the limit to reach, as a list of sizes." + } + }, + "tabular-import": { + "question": "Does my table import survive what real tools export?", + "title": "Tabular import", + "pageTitle": "CSV and Excel Import Test Files - Delimiters, Headers", + "description": "CSV files with other delimiters, CR LF endings, no header and other quoting, a table wider than a spreadsheet shows, an Excel workbook and JSON in several layouts.", + "catches": [ + "a semicolon file read as one column, because the delimiter was assumed rather than looked for", + "a CRLF file split into rows with an empty row after each one", + "a headerless table whose first row of data is eaten as column names", + "an import that keeps the columns it can show and drops the rest without a word", + "a reader that takes JSON records one line at a time and stops at the first indented document" + ], + "details": { + "rows": "How many rows the spreadsheet holds. It is written at exactly the size that many rows package to, so the budget above moves with this.", + "columns": "How many columns each row of the spreadsheet has. Rows times columns has a ceiling, and asking past it is refused before anything is written." + } + }, + "text-encoding": { + "question": "Does my reader know which encoding a file is in, or is it guessing?", + "title": "Text encoding", + "pageTitle": "Text Encoding Test Files - UTF-8, UTF-16, BOM, CRLF", + "description": "The same text in UTF-8, UTF-16LE and UTF-16BE, with and without a byte order mark, and files ending lines in CR LF and LF, to test how a reader decodes text.", + "catches": [ + "a reader that assumes UTF-8 and shows a UTF-16 file as one character in three, or as rows of boxes", + "a byte order mark read as content, so the first field of an import starts with three stray characters", + "an importer that guesses the encoding from the opening bytes and guesses differently for a longer file", + "a CRLF file split into rows with an empty row after each one, or a carriage return kept inside the last field" + ], + "details": { + "sample": "How big each file of the set is. UTF-16 stores two bytes for every character, so an odd number is refused." + } + }, + "upload-validation": { + "question": "Does my upload form take what it should and turn the rest away?", + "title": "Upload validation", + "pageTitle": "Upload Validation Test Files - Type, Size and Name", + "description": "Files for testing an upload form: allowed and denied types, content that does not match its extension, the size limit either side, hostile names and a bulk upload.", + "catches": [ + "a limit enforced in the browser and not on the server", + "an SVG or an HTML file taken for a picture or for plain text, which is a way to get a script past a form", + "a file checked by its extension and never opened, so a PDF named .jpg goes through", + "a form that reads the whole body into memory before it looks at how big it is", + "an upload named PHOTO.JPG turned away where photo.jpg is taken, or the other way round", + "a name with spaces, brackets or characters outside ASCII written to disk unchanged" + ], + "details": { + "limit": "The size limit your upload form declares. This set takes one step either side of it - for a file at every distance, run the size-boundaries preset.", + "allow": "Which types your form is supposed to accept. Each one becomes a real file of that type, and they are the positive control of the whole set.", + "deny": "Which extensions your form is supposed to turn away. An extension this build has no format for still gets a file under that name, holding plain text.", + "far-over": "How far past the limit the one big file goes. Turn it off where writing several times the limit is not worth the disk.", + "bulk": "How many files the mass upload holds. Nought leaves that group out of the set altogether." + } + } }, "commands": { "generate": "produce files, from a recipe or from flags", @@ -115,6 +213,12 @@ "version": "print the tool version", "license": "print the licence and what it means for generated files" }, + "outcomes": { + "accept": "Your system should take the file.", + "reject": "Your system should turn the file away.", + "sanitize": "Your system should take the file and clean it, for example by renaming it.", + "unspecified": "It depends on the rules of your system. You decide, then check that what happens is what you meant." + }, "terms": { "oracleNone": "not applicable", "int": "any whole number", @@ -130,7 +234,14 @@ "hertz": "hertz", "megapixels": "megapixels", "million cells": "million cells", - "entries per second": "entries per second" + "entries per second": "entries per second", + "files": "files", + "sizes separated by commas": "sizes separated by commas", + "format ids separated by commas": "format ids separated by commas", + "format ids separated by commas, or all": "format ids separated by commas, or all", + "extensions separated by commas": "extensions separated by commas", + "the id of a format, as tfg formats lists them": "the id of a format, as tfg formats lists them", + "the password, in plain text": "the password, in plain text" }, "faq": [ { diff --git a/web/content/pl/docs.html b/web/content/pl/docs.html index 27363efb..bfa728a9 100644 --- a/web/content/pl/docs.html +++ b/web/content/pl/docs.html @@ -220,7 +220,9 @@

Czym jest preset?

To gotowy zestaw plików odpowiadający na częste pytanie testowe, żebyś nie musiał projektować zestawu samodzielnie. Presety są pod spodem zwykłymi przepisami, a eject wypisuje ten - przepis, więc możesz go od tego miejsca edytować. + przepis, więc możesz go od tego miejsca edytować. Każdy preset ma + własną stronę: co zwykle znajduje, co jest w zestawie i jakie ustawienia + przyjmuje.

{{ template "presetsList" . }}
tfg preset list
diff --git a/web/content/pl/index.html b/web/content/pl/index.html
index 61fadb10..9ec33daf 100644
--- a/web/content/pl/index.html
+++ b/web/content/pl/index.html
@@ -98,6 +98,18 @@ 

Inne generatory kończą na bajtach. Ten odpowiada na pytanie, które napraw

+
+

Presety

+

Wybierz pytanie, dostań cały zestaw

+

+ Preset to zestaw plików testowych zaprojektowany wokół jednego pytania testowego, więc nie musisz + sam ustalać, który plik czego dowodzi. Każdy ma stronę, która mówi, co zwykle znajduje, co jest + w zestawie i jakie ustawienia przyjmuje. +

+ {{ template "presetsList" . }} +

Wszystkie presety i to, jak mają się do przepisów

+
+

Szybki start

Trzy polecenia, żeby zobaczyć, jak to działa

diff --git a/web/content/pl/preset.html b/web/content/pl/preset.html new file mode 100644 index 00000000..784901aa --- /dev/null +++ b/web/content/pl/preset.html @@ -0,0 +1,91 @@ +{{ with .Preset }} +

{{ $.Up.Label }}

+

{{ .Title }}

+

{{ .Question }}

+

+ Preset {{ .ID }} buduje jednym poleceniem cały zestaw prawdziwych plików testowych do + tego pytania, a obok nich manifest.json, który mówi, jak system ma zareagować na każdy + plik. Wszystko poniżej pochodzi z programu, przy wartościach domyślnych tej wersji. +

+ +{{ if .Catches }} +
+

Co zwykle znajduje?

+
    + {{- range .Catches }} +
  • {{ . }}
  • + {{- end }} +
+
+{{ end }} + +
+

Co jest w zestawie?

+

Przy wartościach domyślnych, tak jak podaje to tfg preset show {{ .ID }}:

+
+ + + + + + + +
Pliki{{ .Budget.Files }}
Cele w przepisie{{ .Budget.Targets }}
Łączny rozmiar{{ .Bytes }} B
Formaty{{ join .Budget.Formats ", " }}
+
+

I czego manifest tego zestawu oczekuje od Twojego systemu:

+
+ + + + + + {{- range .Outcomes }} + + {{- end }} + +
OczekiwanieZnaczeniePliki
{{ .Name }}{{ .Meaning }}{{ .Count }}
+
+
+ +
+

Co można zmienić?

+ {{- if .Settings }} +
+ + + + + + {{- range .Settings }} + + + + + + + {{- end }} + +
UstawieniePrzyjmujeDomyślnieCo robi
--{{ .Flag }}{{ .Takes }}{{ .Default }}{{ .Detail }}{{ if .Placeholder }} Ta wartość domyślna jest naszą wartością zastępczą, a nie wartością Twojego systemu. Podaj własną.{{ end }}
+
+ {{- else }} +

Ten preset nie ma ustawień. Zestaw jest za każdym razem taki sam.

+ {{- end }} +
+ +
+

Jak go uruchomić?

+

Sprawdź, ile zestaw będzie kosztował, zbuduj go albo weź jego przepis do edycji:

+
tfg preset show {{ .ID }}
+tfg generate --preset {{ .ID }}{{ range .Placeholders }} --{{ .Flag }} {{ .Default }}{{ end }} --out ./{{ .ID }}
+tfg preset eject {{ .ID }} > {{ .ID }}.yaml
+

Albo zbuduj na nim własny przepis, trzymany obok testów:

+
version: 1
+extends: preset:{{ .ID }}
+{{- with .Placeholders }}
+with:
+{{- range . }}
+  {{ .Flag }}: {{ .Default }}
+{{- end }}
+{{- end }}
+
+{{ end }} diff --git a/web/content/pl/presets.html b/web/content/pl/presets.html new file mode 100644 index 00000000..dab2ec52 --- /dev/null +++ b/web/content/pl/presets.html @@ -0,0 +1,31 @@ +

Presety plików testowych, jeden zestaw na każde pytanie testowe

+

+ Preset to cały zestaw plików testowych zaprojektowany wokół jednego pytania, z manifestem, który + mówi, jak system ma zareagować na każdy plik. Ty wybierasz pytanie, narzędzie buduje zestaw. Każdy + preset ma własną stronę: co zwykle znajduje, co jest w zestawie i jakie ustawienia przyjmuje. +

+ +{{ template "presetsList" . }} + +
+

Czym preset różni się od przepisu?

+

+ Pod spodem niczym. Preset to przepis, który narzędzie pisze za Ciebie z kilku ustawień. + tfg preset eject wypisuje ten przepis, więc możesz go trzymać obok testów i edytować, + a własny przepis może zbudować na presecie jedną linią, extends: preset: i jego + identyfikator. +

+
tfg preset list
+tfg preset show size-boundaries
+tfg preset eject size-boundaries > my.yaml
+
+ +
+

Czy mogę ufać wartościom domyślnym?

+

+ Co do plików - tak. Co do liczby, którą zna tylko Twój system, na przykład limitu formularza + przesyłania, wartość domyślna jest naszą wartością zastępczą, a narzędzie mówi to za każdym razem, + gdy jej używa. Strona każdego presetu oznacza takie ustawienia, a tfg preset show mówi + o tym, zanim cokolwiek zostanie zapisane. +

+
diff --git a/web/content/pl/site.json b/web/content/pl/site.json index 98a099ff..375eecee 100644 --- a/web/content/pl/site.json +++ b/web/content/pl/site.json @@ -17,6 +17,13 @@ "title": "{{ .Facts.FormatCount }} formatów plików testowych - PDF, DOCX, PNG, ZIP", "description": "Wszystkie formaty, jakie generuje to narzędzie, najmniejszy możliwy plik każdego z nich i ustawienia, które przyjmuje. Każdy otwiera się w swoim programie." }, + { + "key": "presets", + "slug": "presety", + "nav": "Presety", + "title": "Presety plików testowych - gotowe zestawy do testów QA", + "description": "Gotowe zestawy plików testowych, każdy odpowiada na jedno pytanie: limity uploadu, nazwy plików, kodowanie, import tabel, pliki puste i walidacja uploadu." + }, { "key": "docs", "slug": "dokumentacja", @@ -80,7 +87,9 @@ "footerPrivacy": "Ta strona nie ładuje żadnych fontów, skryptów ani liczników z zewnątrz. Nie ustawia ciasteczek.", "notFoundTitle": "Tej strony tu nie ma", "notFoundLead": "Adres, którym tu trafiłeś, nie pasuje do żadnej strony w tym serwisie.", - "notFoundBack": "Wróć na stronę główną" + "notFoundBack": "Wróć na stronę główną", + "read.format": "Format każdego pliku w zestawie. To flaga samego narzędzia, a preset daje jej tylko wartość domyślną.", + "readTakes.format": "identyfikator formatu ze strony formatów" }, "endings": { "0": "Wszystko się udało.", @@ -96,12 +105,101 @@ "143": "Zatrzymane sygnałem - tak wygląda przekroczony czas w CI." }, "presets": { - "empty-and-minimal": "Czy plik poprawny i najmniejszy, na jaki format pozwala, przechodzi?", - "filename-handling": "Czy mój system zapisze, pokaże i odda nazwę pliku, której się nie spodziewał?", - "size-boundaries": "Czy limit rozmiaru działa dokładnie tam, gdzie jest zadeklarowany?", - "tabular-import": "Czy import tabeli poradzi sobie z tym, co eksportują prawdziwe narzędzia?", - "text-encoding": "Czy mój czytnik wie, w jakim kodowaniu jest plik, czy zgaduje?", - "upload-validation": "Czy mój formularz przesyłania plików przyjmuje to, co powinien, i odrzuca resztę?" + "empty-and-minimal": { + "question": "Czy plik poprawny i najmniejszy, na jaki format pozwala, przechodzi?", + "title": "Pliki puste i minimalne", + "pageTitle": "Najmniejsze poprawne i puste pliki w każdym formacie", + "description": "Najmniejszy poprawny plik, jaki to narzędzie zapisze w każdym z {{ .Facts.FormatCount }} formatów, i plik pusty tam, gdzie format na to pozwala, każdy z oczekiwaną reakcją.", + "catches": [ + "poprawny plik odrzucony jako za mały, bo kontrola liczy bajty, zamiast go przeczytać", + "pusty plik, który wywraca czytnik, zamiast zostać zgłoszony", + "obrazek szeroki na jeden piksel, który w drodze do miniatury dzieli przez zero", + "magazyn, który zero bajtów uznaje za nieudane przesłanie i ponawia je bez końca" + ], + "details": { + "formats": "Z jakich formatów powstaje zestaw. Zostaw all, żeby dostać każdy format tej wersji, albo wymień te, które przyjmuje Twój system." + } + }, + "filename-handling": { + "question": "Czy mój system zapisze, pokaże i odda nazwę pliku, której się nie spodziewał?", + "title": "Obsługa nazw plików", + "pageTitle": "Problematyczne nazwy plików do testów - Unicode i długość", + "description": "Pliki z nazwami, które psują przesyłanie i zapis: inne alfabety i emoji, odwrócony kierunek tekstu, znaki niewidoczne, składnia powłoki i SQL, limity długości.", + "catches": [ + "nazwa, która na ekranie, w logu albo na liście wygląda jak inna", + "nazwa ucięta, przycięta albo przepisana między przesłaniem a zapisem", + "limit długości liczony w znakach tam, gdzie magazyn liczy bajty" + ], + "details": {} + }, + "size-boundaries": { + "question": "Czy limit rozmiaru działa dokładnie tam, gdzie jest zadeklarowany?", + "title": "Granice rozmiaru", + "pageTitle": "Test limitu rozmiaru pliku - pliki dokładnie na granicy", + "description": "Pliki o bajt mniejsze od limitu rozmiaru, równe mu i o bajt większe, do tego szersze kroki w obie strony, każdy z informacją, czy system ma go przyjąć.", + "catches": [ + "błędy o jeden na samym limicie", + "pomylone MB i MiB - to 4,8 procent, wystarczy, żeby przepuścić plik, który nie powinien przejść", + "limit sprawdzany w przeglądarce, a nie na serwerze" + ], + "details": { + "limit": "Limit rozmiaru, który deklaruje Twój system. Wszystko inne jest liczone od niego.", + "spread": "Jak daleko w obie strony od limitu sięgać, jako lista rozmiarów." + } + }, + "tabular-import": { + "question": "Czy import tabeli poradzi sobie z tym, co eksportują prawdziwe narzędzia?", + "title": "Import tabel", + "pageTitle": "Pliki testowe do importu CSV i Excel - separatory, nagłówki", + "description": "Pliki CSV z innymi separatorami, końcami linii CR LF, bez nagłówka i z innym cytowaniem, tabela szersza, niż pokaże arkusz, skoroszyt Excel i JSON w kilku układach.", + "catches": [ + "plik ze średnikami wczytany jako jedna kolumna, bo separator założono, zamiast go poszukać", + "plik z CRLF podzielony na wiersze z pustym wierszem po każdym", + "tabela bez nagłówka, której pierwszy wiersz danych zostaje zjedzony jako nazwy kolumn", + "import, który zostawia kolumny, jakie umie pokazać, a resztę bez słowa porzuca", + "czytnik, który bierze rekordy JSON po jednej linii i zatrzymuje się na pierwszym dokumencie z wcięciami" + ], + "details": { + "rows": "Ile wierszy ma arkusz. Plik ma dokładnie taki rozmiar, na jaki pakuje się tyle wierszy, więc budżet zestawu zmienia się razem z tą wartością.", + "columns": "Ile kolumn ma każdy wiersz arkusza. Iloczyn wierszy i kolumn ma sufit, a prośba ponad niego zostaje odrzucona, zanim cokolwiek powstanie." + } + }, + "text-encoding": { + "question": "Czy mój czytnik wie, w jakim kodowaniu jest plik, czy zgaduje?", + "title": "Kodowanie tekstu", + "pageTitle": "Pliki testowe kodowania - UTF-8, UTF-16, BOM, CRLF", + "description": "Ten sam tekst w UTF-8, UTF-16LE i UTF-16BE, ze znacznikiem kolejności bajtów i bez, oraz pliki z końcami linii CR LF i LF, do testu, jak czytnik dekoduje tekst.", + "catches": [ + "czytnik, który zakłada UTF-8 i pokazuje plik UTF-16 jako jeden znak na trzy albo jako rzędy prostokątów", + "znacznik kolejności bajtów wczytany jako treść, więc pierwsze pole importu zaczyna się od trzech obcych znaków", + "import, który zgaduje kodowanie z pierwszych bajtów i dla dłuższego pliku zgaduje inaczej", + "plik z CRLF podzielony na wiersze z pustym wierszem po każdym albo znak powrotu karetki zostawiony w ostatnim polu" + ], + "details": { + "sample": "Jak duży jest każdy plik zestawu. UTF-16 zapisuje dwa bajty na każdy znak, więc liczba nieparzysta zostaje odrzucona." + } + }, + "upload-validation": { + "question": "Czy mój formularz przesyłania plików przyjmuje to, co powinien, i odrzuca resztę?", + "title": "Walidacja uploadu", + "pageTitle": "Pliki do testu walidacji uploadu - typ, rozmiar, nazwa", + "description": "Pliki do testu formularza przesyłania: typy dozwolone i zakazane, treść niezgodna z rozszerzeniem, limit rozmiaru z obu stron, wrogie nazwy i przesyłanie masowe.", + "catches": [ + "limit sprawdzany w przeglądarce, a nie na serwerze", + "plik SVG albo HTML przyjęty jako obrazek albo zwykły tekst, co jest sposobem na przemycenie skryptu przez formularz", + "plik sprawdzany po rozszerzeniu i nigdy nieotwierany, więc PDF nazwany .jpg przechodzi", + "formularz, który wczytuje całe żądanie do pamięci, zanim sprawdzi, jak jest duże", + "plik PHOTO.JPG odrzucony tam, gdzie photo.jpg przechodzi, albo odwrotnie", + "nazwa ze spacjami, nawiasami albo znakami spoza ASCII zapisana na dysk bez zmian" + ], + "details": { + "limit": "Limit rozmiaru, który deklaruje Twój formularz. Ten zestaw robi jeden krok w każdą stronę od niego - po plik w każdej odległości sięgnij po preset size-boundaries.", + "allow": "Które typy formularz ma przyjmować. Każdy staje się prawdziwym plikiem tego typu i to one są kontrolą pozytywną całego zestawu.", + "deny": "Które rozszerzenia formularz ma odrzucać. Rozszerzenie, dla którego ta wersja nie ma formatu, i tak dostaje plik pod tą nazwą, ze zwykłym tekstem w środku.", + "far-over": "Jak daleko za limit sięga jeden duży plik. Wyłącz go tam, gdzie zapis kilkukrotności limitu nie jest wart miejsca na dysku.", + "bulk": "Ile plików ma przesyłanie masowe. Zero całkiem usuwa tę grupę z zestawu." + } + } }, "commands": { "generate": "tworzy pliki, z przepisu albo z flag", @@ -115,6 +213,12 @@ "version": "wypisuje wersję narzędzia", "license": "wypisuje licencję i to, co znaczy dla wygenerowanych plików" }, + "outcomes": { + "accept": "System powinien przyjąć plik.", + "reject": "System powinien odrzucić plik.", + "sanitize": "System powinien przyjąć plik i go oczyścić, na przykład zmieniając mu nazwę.", + "unspecified": "Zależy od reguł Twojego systemu. Ty decydujesz, a potem sprawdzasz, czy dzieje się to, co zamierzałeś." + }, "terms": { "oracleNone": "nie dotyczy", "int": "dowolna liczba całkowita", @@ -130,7 +234,14 @@ "hertz": "herców", "megapixels": "megapikseli", "million cells": "milionów komórek", - "entries per second": "wpisów na sekundę" + "entries per second": "wpisów na sekundę", + "files": "plików", + "sizes separated by commas": "rozmiary rozdzielone przecinkami", + "format ids separated by commas": "identyfikatory formatów rozdzielone przecinkami", + "format ids separated by commas, or all": "identyfikatory formatów rozdzielone przecinkami albo all", + "extensions separated by commas": "rozszerzenia rozdzielone przecinkami", + "the id of a format, as tfg formats lists them": "identyfikator formatu, tak jak wypisuje go tfg formats", + "the password, in plain text": "hasło, zwykłym tekstem" }, "faq": [ { diff --git a/web/public/404.html b/web/public/404.html index 469c1108..4f092c3c 100644 --- a/web/public/404.html +++ b/web/public/404.html @@ -39,6 +39,7 @@