Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,26 @@ because it turns other people's test suites red.

### Added

- **SVG drawings can be any size you ask for.** Two new settings on `svg`:
`width` and `height`, both a whole number of pixels from 1 to 20000. They
default to the 800 by 600 these drawings have always been, so a recipe that
says nothing gets the same bytes it got before.

```
tfg generate --format svg --size 20kb --set width=1920 --set height=1080
```

There is no joint limit on the two, unlike the picture formats, because
nothing is drawn into pixels here - the file only says how big it is. That
makes a small file that claims to be enormous, which is the point: a 3 kB
drawing declaring 20000 by 20000 asks whether whatever opens it has a limit
on picture size and not only on file size. Pillow, for one, refuses to open
the result.

A drawing shorter than 57 pixels has no room for the label along its bottom
edge. It is still produced and still named, and the run says which files
those were.

- **Text and Markdown files can be written in UTF-16, with or without a byte
order mark.** Two new settings on `txt` and `md`: `encoding`, which takes
`utf-8`, `utf-16le` or `utf-16be`, and `bom`, which is `true` or `false`.
Expand Down Expand Up @@ -191,6 +211,20 @@ because it turns other people's test suites red.

### Changed

- **Byte counts are grouped in threes.** A total used to print as
`2516582400 B`. It now prints as `2 516 582 400 B`, in every message that
names a number of bytes - `tfg formats`, the summary a run prints, what a
preset says its budget is, and what `tfg validate` reports.

Grouped with a space rather than a comma, because a comma is a thousands mark
in some countries and a decimal point in others, and this tool is read in
both.

Machine output is untouched. `--json` and the manifest carry numbers rather
than sentences, so nothing that parses those sees any of this. If you have a
script reading a byte count out of the human output, it needs to take the
spaces out.

- **Notes are reported once per thing they say, not once per file.** A run of
25 000 one-byte text files used to print 25 001 `note:` lines, every one of
them the same sentence about the label not fitting. It now prints one, with
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,8 @@ recipe. `tfg formats <id>` prints the allowed range or list for each:
| `log` | `entry_format`, `timestamps`, `rate`, `methods`, `status_mix`, `level_mix`, `ip_version`, `line_ending` |
| `txt`, `md` | `encoding`, `bom` |
| `json` | `formatting` |
| `xml`, `html`, `svg` | none |
| `svg` | `width`, `height` |
| `xml`, `html` | none |

```
tfg generate --format jpg --size 500kb --set width=1920 --set height=1080 --set quality=85
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/formats.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"

"github.com/donislawdev/TestingFilesGenerator/internal/core"
"github.com/donislawdev/TestingFilesGenerator/internal/format"
)

Expand Down Expand Up @@ -109,8 +110,8 @@ func entryFor(d format.Descriptor) formatEntry {
// list and ignore the argument, ending with 0 - so there was no way to ask what
// a format accepts, and the silence looked like an answer.
func describeOne(d format.Descriptor, out io.Writer) {
fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %d B\n",
d.ID, d.Fidelity, d.Determinism, smallestAccepted(d))
fmt.Fprintf(out, "%s - %s fidelity, %s deterministic, minimum %s\n",
d.ID, d.Fidelity, d.Determinism, core.ExactBytes(smallestAccepted(d)))
fmt.Fprintf(out, " extension %s\n", d.Extension)
fmt.Fprintf(out, " padding %s\n", d.Padding.Name)
fmt.Fprintf(out, " label %s\n", d.Label)
Expand Down
14 changes: 10 additions & 4 deletions internal/cli/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,10 +351,16 @@ func sizesFromFlags(g *generateOpts, errOut io.Writer) (sizes []int64, low, high
if errors.Is(err, core.ErrBoundaryTooSmall) {
// A number somebody typed, so this is USAGE rather than a problem
// with a document. The end above it keeps the code it had.
// Ungrouped, unlike every other count this program prints, and the
// exception is deliberate: this echoes back the number somebody
// typed after --boundary. A message that quotes your input and
// respells it on the way is a message you have to translate before
// you can compare it with what you wrote.
fmt.Fprintf(errOut, "tfg: --boundary %d B is too small - %s\n", limit, err)
return nil, 0, 0, 0, ExitUsage
}
if err != nil {
// Ungrouped for the same reason as the line above it.
fmt.Fprintf(errOut, "tfg: --boundary %d B is too large - %s\n", limit, err)
return nil, 0, 0, 0, ExitRecipe
}
Expand Down Expand Up @@ -387,9 +393,9 @@ func produce(ctx context.Context, targets []engine.Target, opt engine.Options, g
// Echo the exact byte count. The exact number is the point of this tool,
// and it is what any other tool will show when the user goes to check the
// file.
fmt.Fprintf(errOut, "%s in %s, %d B total\n",
fmt.Fprintf(errOut, "%s in %s, %s total\n",
core.Count(len(planned), "file", "files"), core.Count(len(targets), "target", "targets"),
engine.TotalBytes(planned))
core.ExactBytes(engine.TotalBytes(planned)))

echoBoundaries(targets, planned, errOut)
echoManifestReach(planned, errOut)
Expand Down Expand Up @@ -530,10 +536,10 @@ func echoBoundaries(targets []engine.Target, planned []engine.PlannedFile, errOu
if t.BoundaryLimit <= 0 {
continue
}
fmt.Fprintf(errOut, "boundary %q around %d B:\n", t.ID, t.BoundaryLimit)
fmt.Fprintf(errOut, "boundary %q around %s:\n", t.ID, core.ExactBytes(t.BoundaryLimit))
for _, f := range planned {
if f.Target == t {
fmt.Fprintf(errOut, " %-26s %d B\n", f.Name, f.Plan.Bytes)
fmt.Fprintf(errOut, " %-26s %s\n", f.Name, core.ExactBytes(f.Plan.Bytes))
}
}

Expand Down
4 changes: 2 additions & 2 deletions internal/cli/preset.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,9 +537,9 @@ func describePreset(e *preset.Expansion, b budget, out io.Writer) {
fmt.Fprintf(out, " --%-12s the global flag, this preset gives it a default\n", name)
}

fmt.Fprintf(out, "\nbudget at these values:\n %s, %s, %d B total, format %s\n",
fmt.Fprintf(out, "\nbudget at these values:\n %s, %s, %s total, format %s\n",
core.Count(b.Targets, "target", "targets"), core.Count(b.Files, "file", "files"),
b.Bytes, strings.Join(b.Formats, ", "))
core.ExactBytes(b.Bytes), strings.Join(b.Formats, ", "))
for _, note := range e.Notes() {
fmt.Fprintf(out, "\nnote: %s\n", note)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/recipecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ func validate(ctx context.Context, args []string, out, errOut io.Writer) int {
}, ExitOK)
}

fmt.Fprintf(out, "%s is valid: %s, %s, %d B total\n%s\n",
path, core.Count(len(rec.Targets), "target", "targets"), core.Count(len(planned), "file", "files"), engine.TotalBytes(planned), hash)
fmt.Fprintf(out, "%s is valid: %s, %s, %s total\n%s\n",
path, core.Count(len(rec.Targets), "target", "targets"), core.Count(len(planned), "file", "files"),
core.ExactBytes(engine.TotalBytes(planned)), hash)
return ExitOK
}

Expand Down
56 changes: 55 additions & 1 deletion internal/core/humanise.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package core
import (
"fmt"
"math"
"strconv"
"strings"
"time"
)

Expand All @@ -20,7 +22,10 @@ import (
func HumanBytes(n int64) string {
const unit = 1024
if n < unit {
return fmt.Sprintf("%d B", n)
// Through ExactBytes rather than its own %d, so the two never spell one
// number two ways. Below 1024 there is nothing to group, which is
// exactly why this is easy to get wrong and leave wrong.
return ExactBytes(n)
}
div, exp := int64(unit), 0
for n/div >= unit && exp < 3 {
Expand All @@ -30,6 +35,55 @@ func HumanBytes(n int64) string {
return fmt.Sprintf("%.1f %cB", float64(n)/float64(div), "KMGT"[exp])
}

// ExactBytes writes a count out in full, grouped in threes, with its unit.
//
// The exact number is the point of this tool and it can never be replaced by a
// rounded one - but eleven digits in a row is a number nobody reads, and both
// surfaces printed it that way. "2516582400 B" was measured on the window's run
// panel and on four lines of the command line, and the owner's report of it was
// that the bytes are welcome and unreadable, which are both true at once.
//
// Grouped with a space rather than a comma, and that is the one choice here
// worth writing down. A comma is the thousands mark in English and the decimal
// mark for most of Europe, so "2,516" is either two and a half thousand or two
// and a half depending on who is reading - and the people who read this run it
// in every country. A space means the same thing everywhere.
//
// Machine output is untouched on purpose. Nothing in a manifest or under --json
// goes through here, because a number there is a number and not a sentence.
func ExactBytes(n int64) string {
return groupedInThrees(strconv.FormatInt(n, 10)) + " B"
}

// groupedInThrees puts a space every three digits, counting from the right.
//
// Written out rather than reached for in a library because the one in the
// standard library is about money: golang.org/x/text/message formats to a
// LOCALE, and a locale is exactly what this must not have - the window and the
// command line have to say the same thing on a Polish desktop and an American
// one, and docs/UX.md has the surfaces agreeing as a rule rather than a hope.
func groupedInThrees(digits string) string {
sign := ""
if strings.HasPrefix(digits, "-") {
sign, digits = "-", digits[1:]
}
if len(digits) <= 3 {
return sign + digits
}
lead := len(digits) % 3
if lead == 0 {
lead = 3
}
var out strings.Builder
out.Grow(len(digits) + (len(digits)-1)/3)
out.WriteString(digits[:lead])
for i := lead; i < len(digits); i += 3 {
out.WriteByte(' ')
out.WriteString(digits[i : i+3])
}
return sign + out.String()
}

// Percent divides before multiplying where it has to, so a very large run does
// not wrap on the way to a number between nought and a hundred.
//
Expand Down
Loading
Loading