Skip to content

Commit ef2e752

Browse files
donislawdevclaude
andcommitted
core: count every size unit in 1024s, against the standard and on purpose
Measured on Windows and on Linux. A file of 10 485 760 bytes is shown as "10 MB" by Windows Explorer and "10M" by ls -lh. A file of 10 000 000 bytes is shown as "9,53 MB" and "9.6M". The tester does not check the file with our tool. They check it with the file manager or with ls. Under the SI reading, asking for 10mb produced a file the system calls 9,53 MB, and the honest conclusion for anybody looking at that is that the generator is broken. For a tool whose only promise is an exact size, looking broken costs more than following the standard buys. So kb, mb, gb and tb count in 1024s, and kib, mib, gib and tib are spelled out versions of the same thing. Somebody who genuinely wants ten million bytes writes the number, and the exact byte count is echoed on every run. Rejected: keeping the standard reading, and refusing ambiguous units outright. The second one never hands back a size nobody asked for, but an error on the most common input is itself confusing to someone who does not know the units - and those are exactly the people this protects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c552adf commit ef2e752

4 files changed

Lines changed: 133 additions & 29 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@ because it turns other people's test suites red.
4343

4444
### Notes on behaviour
4545

46-
- **Sizes.** `mb`, `kb`, `gb` and `tb` count in thousands. `mib`, `kib`,
47-
`gib` and `tib` count in 1024s. So `10mb` is 10 000 000 bytes and `10mib`
48-
is 10 485 760. A size that does not land on a whole byte is an error rather
49-
than a rounded number.
46+
- **Sizes count in 1024s.** `10mb` is 10 485 760 bytes, which is what
47+
Windows Explorer and `ls -lh` both call "10 MB". `mib`, `kib` and `gib` are
48+
spelled out versions of the same thing. This departs from the SI standard
49+
on purpose - a file you asked for as 10mb has to look like 10 MB when you
50+
check it. Write the number if you want exactly ten million bytes. A size
51+
that does not land on a whole byte is an error rather than a rounded
52+
number.
5053
- **`--size` is required.** Every target declares its size, which is what
5154
lets `--dry-run` report exact numbers before anything reaches the disk.
5255
- **The label needs room.** A text file smaller than the label carries no

‎internal/cli/cli.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ func generate(ctx context.Context, args []string, out, errOut io.Writer) int {
8585

8686
var (
8787
formatID = fs.String("format", "", "format of the files to produce, for example txt")
88-
sizeStr = fs.String("size", "", "exact size of each file, for example 10mb, 1.5gib or 10485761")
88+
sizeStr = fs.String("size", "", "exact size of each file. Units count in 1024s, so 10mb is 10485760 bytes. Also accepts a plain byte count")
8989
count = fs.Int("count", 1, "how many files to produce")
9090
outDir = fs.String("out", ".", "directory to write into")
9191
name = fs.String("name", "", "name template, for example invoice_{index:04}.txt")

‎internal/core/size.go‎

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@ import (
99

1010
// ParseSize turns a human written size into an exact number of bytes.
1111
//
12-
// Decimal units count in thousands and binary units count in 1024s, the way
13-
// the standards define them:
14-
//
15-
// 10mb -> 10 000 000
16-
// 10mib -> 10 485 760
17-
// 1.5gib -> 1 610 612 736
18-
// 700kB -> 700 000
12+
// 10mb -> 10 485 760
13+
// 10mib -> 10 485 760 (the same, spelled out)
14+
// 1.5gib -> 1 610 612 736
15+
// 700kb -> 716 800
1916
// 10485761 -> 10 485 761
2017
//
2118
// Case does not matter. A plain number is a count of bytes.
2219
//
20+
// Every unit counts in 1024s, and that is a deliberate departure from the
21+
// standards. The reason is what the person checking the file will see.
22+
// Measured on Windows and on Linux: a file of 10 485 760 bytes is shown as
23+
// "10 MB" by Windows Explorer and as "10M" by ls, while a file of 10 000 000
24+
// bytes is shown as "9,53 MB" and "9.6M". Someone who asks for 10mb, gets ten
25+
// million bytes and then looks at the file concludes the generator is broken
26+
// - and for a tool whose whole promise is an exact size, that costs more than
27+
// following the standard buys.
28+
//
29+
// Someone who genuinely wants ten million bytes writes the number.
30+
//
2331
// A size that does not land on a whole byte is refused rather than rounded.
2432
// Rounding is the one thing this tool must never do quietly - a batch of ten
2533
// thousand files would bury the warning and hand back data nobody asked for.
@@ -77,34 +85,24 @@ func ParseSize(s string) (int64, error) {
7785

7886
func unitBytes(unit string) (int64, bool) {
7987
const (
80-
k = 1000
81-
m = k * 1000
82-
g = m * 1000
83-
t = g * 1000
84-
8588
ki = 1024
8689
mi = ki * 1024
8790
gi = mi * 1024
8891
ti = gi * 1024
8992
)
93+
// The two spellings mean the same thing. kib and mib exist because people
94+
// who know the difference reach for them, not because they behave
95+
// differently here.
9096
switch unit {
9197
case "", "b":
9298
return 1, true
93-
case "k", "kb":
94-
return k, true
95-
case "m", "mb":
96-
return m, true
97-
case "g", "gb":
98-
return g, true
99-
case "t", "tb":
100-
return t, true
101-
case "ki", "kib":
99+
case "k", "kb", "ki", "kib":
102100
return ki, true
103-
case "mi", "mib":
101+
case "m", "mb", "mi", "mib":
104102
return mi, true
105-
case "gi", "gib":
103+
case "g", "gb", "gi", "gib":
106104
return gi, true
107-
case "ti", "tib":
105+
case "t", "tb", "ti", "tib":
108106
return ti, true
109107
}
110108
return 0, false

‎internal/guard/size_test.go‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package guard
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/donislawdev/TestingFilesGenerator/internal/core"
8+
)
9+
10+
// Sizes count in 1024s, on purpose and against the standards.
11+
//
12+
// Measured: Windows Explorer shows 10 485 760 bytes as "10 MB" and 10 000 000
13+
// bytes as "9,53 MB". ls -lh shows "10M" and "9.6M". A tester who asks for
14+
// 10mb and then checks the file has to see the number they asked for, or the
15+
// tool looks broken - and looking broken costs more than following the
16+
// standard buys.
17+
//
18+
// Someone who wants exactly ten million bytes writes the number.
19+
20+
func TestSizesCountIn1024s(t *testing.T) {
21+
cases := []struct {
22+
in string
23+
want int64
24+
}{
25+
{"1024", 1024},
26+
{"0", 0},
27+
{"1b", 1},
28+
{"1kb", 1024},
29+
{"1kib", 1024},
30+
{"1k", 1024},
31+
{"1mb", 1024 * 1024},
32+
{"1mib", 1024 * 1024},
33+
{"10mb", 10 * 1024 * 1024},
34+
{"1gb", 1024 * 1024 * 1024},
35+
{"1gib", 1024 * 1024 * 1024},
36+
{"1.5gib", 1610612736},
37+
{"1tb", 1024 * 1024 * 1024 * 1024},
38+
39+
// Case does not matter, and neither does a space.
40+
{"700kB", 700 * 1024},
41+
{"10 MB", 10 * 1024 * 1024},
42+
{"10MiB", 10 * 1024 * 1024},
43+
44+
// The size the whole project keeps as its example.
45+
{"10485761", 10485761},
46+
}
47+
48+
for _, c := range cases {
49+
got, err := core.ParseSize(c.in)
50+
if err != nil {
51+
t.Errorf("%q failed: %v", c.in, err)
52+
continue
53+
}
54+
if got != c.want {
55+
t.Errorf("%q gave %d B, expected %d B", c.in, got, c.want)
56+
}
57+
}
58+
}
59+
60+
func TestTheTwoSpellingsAgree(t *testing.T) {
61+
pairs := [][2]string{
62+
{"5kb", "5kib"},
63+
{"5mb", "5mib"},
64+
{"5gb", "5gib"},
65+
{"5tb", "5tib"},
66+
}
67+
for _, p := range pairs {
68+
a, errA := core.ParseSize(p[0])
69+
b, errB := core.ParseSize(p[1])
70+
if errA != nil || errB != nil {
71+
t.Fatalf("%q or %q failed: %v %v", p[0], p[1], errA, errB)
72+
}
73+
if a != b {
74+
t.Errorf("%q gave %d B and %q gave %d B - the two spellings mean the same thing", p[0], a, p[1], b)
75+
}
76+
}
77+
}
78+
79+
func TestASizeThatIsNotAWholeByteIsRefused(t *testing.T) {
80+
// Rounding is the one thing this tool must never do quietly. In a batch
81+
// of ten thousand files a warning is lost, and the user silently receives
82+
// data they did not order.
83+
for _, in := range []string{"1.5", "0.5b", "1.0001kb"} {
84+
got, err := core.ParseSize(in)
85+
if err == nil {
86+
t.Errorf("%q was accepted as %d B instead of being refused", in, got)
87+
continue
88+
}
89+
// The message has to say what a workable value looks like, not only
90+
// that the input was wrong.
91+
if !strings.Contains(err.Error(), "whole number of bytes") {
92+
t.Errorf("%q was refused without explaining why: %v", in, err)
93+
}
94+
}
95+
}
96+
97+
func TestNonsenseSizesAreRefusedWithAUsefulMessage(t *testing.T) {
98+
for _, in := range []string{"", "banana", "mb", "10qb", "-5mb"} {
99+
if got, err := core.ParseSize(in); err == nil {
100+
t.Errorf("%q was accepted as %d B", in, got)
101+
}
102+
}
103+
}

0 commit comments

Comments
 (0)