fix(config): keep saved scale in its config group (#28)
- commit
- 77b3aaec09d3764c427d028d6c2d1a8af649aa0b
- author
- bnema · · 2026-09-27 16:36 UTC
- committer
- GitHub · · 2026-09-27 16:36 UTC
- parent
- 089984d
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
2 files · +85 -18
M internal/adapters/config/scalestore.go +68 -15
| @@ -11,13 +11,12 @@ import ( | |||
| 11 | 11 | "strings" | |
| 12 | 12 | ) | |
| 13 | 13 | ||
| 14 | - | // ScaleStore writes output.<name>.scale into the config file. It edits the | |
| 15 | - | // value in place and keeps every other line, comments included. | |
| 14 | + | // ScaleStore writes output.<name>.scale into the config file, preserving | |
| 15 | + | // unrelated lines and comments. | |
| 16 | 16 | type ScaleStore struct{ Path string } | |
| 17 | 17 | ||
| 18 | - | // SaveOutputScale sets output.<name>.scale, appending the key when the file | |
| 19 | - | // has none. The file is replaced atomically; a symlinked config is written | |
| 20 | - | // through to its target. | |
| 18 | + | // SaveOutputScale updates output.<name>.scale in the config file. The file is | |
| 19 | + | // replaced atomically; a symlinked config is written through to its target. | |
| 21 | 20 | func (s ScaleStore) SaveOutputScale(output string, scale float64) error { | |
| 22 | 21 | path := s.Path | |
| 23 | 22 | if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 { | |
| @@ -44,21 +43,66 @@ func (s ScaleStore) SaveOutputScale(output string, scale float64) error { | |||
| 44 | 43 | return writeAtomic(path, setKey(data, "output."+output+".scale", formatScale(scale)), mode) | |
| 45 | 44 | } | |
| 46 | 45 | ||
| 47 | - | // setKey replaces the value of the last line setting key (the one parse | |
| 48 | - | // keeps) or appends key = value. | |
| 46 | + | // setKey replaces the last active assignment, or enables a matching commented | |
| 47 | + | // assignment. New keys go beside the closest related key, then at the end. | |
| 49 | 48 | func setKey(data []byte, key, value string) []byte { | |
| 50 | 49 | lines := strings.SplitAfter(string(data), "\n") | |
| 50 | + | commented := -1 | |
| 51 | + | for i, line := range lines { | |
| 52 | + | body := strings.TrimSpace(line) | |
| 53 | + | if !strings.HasPrefix(body, "#") { | |
| 54 | + | continue | |
| 55 | + | } | |
| 56 | + | body = strings.TrimSpace(strings.TrimPrefix(body, "#")) | |
| 57 | + | k, _, ok := strings.Cut(body, "=") | |
| 58 | + | if ok && strings.TrimSpace(k) == key { | |
| 59 | + | commented = i | |
| 60 | + | } | |
| 61 | + | } | |
| 51 | 62 | for i := len(lines) - 1; i >= 0; i-- { | |
| 52 | - | k, v, ok := strings.Cut(lines[i], "=") | |
| 53 | - | if !ok || strings.TrimSpace(k) != key { | |
| 63 | + | prefix, rest, ok := strings.Cut(lines[i], "=") | |
| 64 | + | if !ok || strings.TrimSpace(prefix) != key { | |
| 65 | + | continue | |
| 66 | + | } | |
| 67 | + | lines[i] = replaceValue(prefix+"=", rest, value) | |
| 68 | + | return []byte(strings.Join(lines, "")) | |
| 69 | + | } | |
| 70 | + | if commented >= 0 { | |
| 71 | + | line := lines[commented] | |
| 72 | + | indent, body, _ := strings.Cut(line, "#") | |
| 73 | + | prefix, rest, _ := strings.Cut(strings.TrimLeft(body, " \t"), "=") | |
| 74 | + | lines[commented] = replaceValue(indent+prefix+"=", rest, value) | |
| 75 | + | return []byte(strings.Join(lines, "")) | |
| 76 | + | } | |
| 77 | + | // Match the longest dotted parent first: output.DP-2.scale belongs beside | |
| 78 | + | // output.DP-2.*, not merely beside any output.* key. | |
| 79 | + | for parent := key; ; { | |
| 80 | + | dot := strings.LastIndexByte(parent, '.') | |
| 81 | + | if dot < 0 { | |
| 82 | + | break | |
| 83 | + | } | |
| 84 | + | parent = parent[:dot] | |
| 85 | + | index := -1 | |
| 86 | + | for i, line := range lines { | |
| 87 | + | k, _, ok := strings.Cut(line, "=") | |
| 88 | + | k = strings.TrimSpace(k) | |
| 89 | + | if ok && (k == parent || strings.HasPrefix(k, parent+".")) { | |
| 90 | + | index = i | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | if index < 0 { | |
| 54 | 94 | continue | |
| 55 | 95 | } | |
| 56 | - | // Keep the spacing around = and an end-of-line comment. | |
| 57 | - | rest := strings.TrimRight(v, "\r\n") | |
| 58 | - | eol := v[len(rest):] | |
| 59 | - | old := stripComment(strings.TrimSpace(rest)) | |
| 60 | - | start := strings.Index(rest, old) | |
| 61 | - | lines[i] = k + "=" + rest[:start] + value + rest[start+len(old):] + eol | |
| 96 | + | eol := "\n" | |
| 97 | + | if strings.HasSuffix(lines[index], "\r\n") { | |
| 98 | + | eol = "\r\n" | |
| 99 | + | } | |
| 100 | + | if !strings.HasSuffix(lines[index], "\n") { | |
| 101 | + | lines[index] += eol | |
| 102 | + | } | |
| 103 | + | lines = append(lines, "") | |
| 104 | + | copy(lines[index+2:], lines[index+1:]) | |
| 105 | + | lines[index+1] = key + " = " + value + eol | |
| 62 | 106 | return []byte(strings.Join(lines, "")) | |
| 63 | 107 | } | |
| 64 | 108 | var b bytes.Buffer | |
| @@ -70,6 +114,15 @@ func setKey(data []byte, key, value string) []byte { | |||
| 70 | 114 | return b.Bytes() | |
| 71 | 115 | } | |
| 72 | 116 | ||
| 117 | + | // replaceValue keeps indentation, spacing and trailing comments in an assignment. | |
| 118 | + | func replaceValue(prefix, rest, value string) string { | |
| 119 | + | plain := strings.TrimRight(rest, "\r\n") | |
| 120 | + | eol := rest[len(plain):] | |
| 121 | + | old := stripComment(strings.TrimSpace(plain)) | |
| 122 | + | start := strings.Index(plain, old) | |
| 123 | + | return prefix + plain[:start] + value + plain[start+len(old):] + eol | |
| 124 | + | } | |
| 125 | + | ||
| 73 | 126 | // formatScale writes scales as the config expects them: a decimal when it is | |
| 74 | 127 | // exact (1.25), else a fraction (4/3). | |
| 75 | 128 | func formatScale(s float64) string { | |
| @@ -11,13 +11,12 @@ import ( | |||||
| 11 | "strings" | 11 | "strings" | ||
| 12 | ) | 12 | ) | ||
| 13 | 13 | ||||
| 14 | - | // ScaleStore writes output.<name>.scale into the config file. It edits the | 14 | + | // ScaleStore writes output.<name>.scale into the config file, preserving |
| 15 | - | // value in place and keeps every other line, comments included. | 15 | + | // unrelated lines and comments. |
| 16 | type ScaleStore struct{ Path string } | 16 | type ScaleStore struct{ Path string } | ||
| 17 | 17 | ||||
| 18 | - | // SaveOutputScale sets output.<name>.scale, appending the key when the file | 18 | + | // SaveOutputScale updates output.<name>.scale in the config file. The file is |
| 19 | - | // has none. The file is replaced atomically; a symlinked config is written | 19 | + | // replaced atomically; a symlinked config is written through to its target. |
| 20 | - | // through to its target. | |||
| 21 | func (s ScaleStore) SaveOutputScale(output string, scale float64) error { | 20 | func (s ScaleStore) SaveOutputScale(output string, scale float64) error { | ||
| 22 | path := s.Path | 21 | path := s.Path | ||
| 23 | if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 { | 22 | if info, err := os.Lstat(path); err == nil && info.Mode()&os.ModeSymlink != 0 { | ||
| @@ -44,21 +43,66 @@ func (s ScaleStore) SaveOutputScale(output string, scale float64) error { | |||||
| 44 | return writeAtomic(path, setKey(data, "output."+output+".scale", formatScale(scale)), mode) | 43 | return writeAtomic(path, setKey(data, "output."+output+".scale", formatScale(scale)), mode) | ||
| 45 | } | 44 | } | ||
| 46 | 45 | ||||
| 47 | - | // setKey replaces the value of the last line setting key (the one parse | 46 | + | // setKey replaces the last active assignment, or enables a matching commented |
| 48 | - | // keeps) or appends key = value. | 47 | + | // assignment. New keys go beside the closest related key, then at the end. |
| 49 | func setKey(data []byte, key, value string) []byte { | 48 | func setKey(data []byte, key, value string) []byte { | ||
| 50 | lines := strings.SplitAfter(string(data), "\n") | 49 | lines := strings.SplitAfter(string(data), "\n") | ||
| 50 | + | commented := -1 | |||
| 51 | + | for i, line := range lines { | |||
| 52 | + | body := strings.TrimSpace(line) | |||
| 53 | + | if !strings.HasPrefix(body, "#") { | |||
| 54 | + | continue | |||
| 55 | + | } | |||
| 56 | + | body = strings.TrimSpace(strings.TrimPrefix(body, "#")) | |||
| 57 | + | k, _, ok := strings.Cut(body, "=") | |||
| 58 | + | if ok && strings.TrimSpace(k) == key { | |||
| 59 | + | commented = i | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 51 | for i := len(lines) - 1; i >= 0; i-- { | 62 | for i := len(lines) - 1; i >= 0; i-- { | ||
| 52 | - | k, v, ok := strings.Cut(lines[i], "=") | 63 | + | prefix, rest, ok := strings.Cut(lines[i], "=") |
| 53 | - | if !ok || strings.TrimSpace(k) != key { | 64 | + | if !ok || strings.TrimSpace(prefix) != key { |
| 65 | + | continue | |||
| 66 | + | } | |||
| 67 | + | lines[i] = replaceValue(prefix+"=", rest, value) | |||
| 68 | + | return []byte(strings.Join(lines, "")) | |||
| 69 | + | } | |||
| 70 | + | if commented >= 0 { | |||
| 71 | + | line := lines[commented] | |||
| 72 | + | indent, body, _ := strings.Cut(line, "#") | |||
| 73 | + | prefix, rest, _ := strings.Cut(strings.TrimLeft(body, " \t"), "=") | |||
| 74 | + | lines[commented] = replaceValue(indent+prefix+"=", rest, value) | |||
| 75 | + | return []byte(strings.Join(lines, "")) | |||
| 76 | + | } | |||
| 77 | + | // Match the longest dotted parent first: output.DP-2.scale belongs beside | |||
| 78 | + | // output.DP-2.*, not merely beside any output.* key. | |||
| 79 | + | for parent := key; ; { | |||
| 80 | + | dot := strings.LastIndexByte(parent, '.') | |||
| 81 | + | if dot < 0 { | |||
| 82 | + | break | |||
| 83 | + | } | |||
| 84 | + | parent = parent[:dot] | |||
| 85 | + | index := -1 | |||
| 86 | + | for i, line := range lines { | |||
| 87 | + | k, _, ok := strings.Cut(line, "=") | |||
| 88 | + | k = strings.TrimSpace(k) | |||
| 89 | + | if ok && (k == parent || strings.HasPrefix(k, parent+".")) { | |||
| 90 | + | index = i | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | if index < 0 { | |||
| 54 | continue | 94 | continue | ||
| 55 | } | 95 | } | ||
| 56 | - | // Keep the spacing around = and an end-of-line comment. | 96 | + | eol := "\n" |
| 57 | - | rest := strings.TrimRight(v, "\r\n") | 97 | + | if strings.HasSuffix(lines[index], "\r\n") { |
| 58 | - | eol := v[len(rest):] | 98 | + | eol = "\r\n" |
| 59 | - | old := stripComment(strings.TrimSpace(rest)) | 99 | + | } |
| 60 | - | start := strings.Index(rest, old) | 100 | + | if !strings.HasSuffix(lines[index], "\n") { |
| 61 | - | lines[i] = k + "=" + rest[:start] + value + rest[start+len(old):] + eol | 101 | + | lines[index] += eol |
| 102 | + | } | |||
| 103 | + | lines = append(lines, "") | |||
| 104 | + | copy(lines[index+2:], lines[index+1:]) | |||
| 105 | + | lines[index+1] = key + " = " + value + eol | |||
| 62 | return []byte(strings.Join(lines, "")) | 106 | return []byte(strings.Join(lines, "")) | ||
| 63 | } | 107 | } | ||
| 64 | var b bytes.Buffer | 108 | var b bytes.Buffer | ||
| @@ -70,6 +114,15 @@ func setKey(data []byte, key, value string) []byte { | |||||
| 70 | return b.Bytes() | 114 | return b.Bytes() | ||
| 71 | } | 115 | } | ||
| 72 | 116 | ||||
| 117 | + | // replaceValue keeps indentation, spacing and trailing comments in an assignment. | |||
| 118 | + | func replaceValue(prefix, rest, value string) string { | |||
| 119 | + | plain := strings.TrimRight(rest, "\r\n") | |||
| 120 | + | eol := rest[len(plain):] | |||
| 121 | + | old := stripComment(strings.TrimSpace(plain)) | |||
| 122 | + | start := strings.Index(plain, old) | |||
| 123 | + | return prefix + plain[:start] + value + plain[start+len(old):] + eol | |||
| 124 | + | } | |||
| 125 | + | ||||
| 73 | // formatScale writes scales as the config expects them: a decimal when it is | 126 | // formatScale writes scales as the config expects them: a decimal when it is | ||
| 74 | // exact (1.25), else a fraction (4/3). | 127 | // exact (1.25), else a fraction (4/3). | ||
| 75 | func formatScale(s float64) string { | 128 | func formatScale(s float64) string { | ||
M internal/adapters/config/scalestore_test.go +17 -3
| @@ -15,11 +15,18 @@ func TestSaveOutputScale(t *testing.T) { | |||
| 15 | 15 | {"missing file", "", "output.DP-2.scale = 1.5\n", 1.5}, | |
| 16 | 16 | {"append", "# my config\nborder.width = 3", "# my config\nborder.width = 3\noutput.DP-2.scale = 1.5\n", 1.5}, | |
| 17 | 17 | {"replace in place", "output.DP-2.scale = 2 # big\n# end\n", "output.DP-2.scale = 1.25 # big\n# end\n", 1.25}, | |
| 18 | + | {"enable commented value", "# output.DP-2.scale = 4/3 # 4K display\nrender.vrr = on\n", "output.DP-2.scale = 1.5 # 4K display\nrender.vrr = on\n", 1.5}, | |
| 19 | + | {"enable indented CRLF example", " # output.DP-2.scale = 2 # note\r\nrender.vrr = on\r\n", " output.DP-2.scale = 1.5 # note\r\nrender.vrr = on\r\n", 1.5}, | |
| 20 | + | {"insert with same output group", "output.DP-2.hdr = off\noutput.DP-1 = off\n", "output.DP-2.hdr = off\noutput.DP-2.scale = 1.5\noutput.DP-1 = off\n", 1.5}, | |
| 21 | + | {"insert with output group", "output.DP-1 = off\nrender.vrr = on\n", "output.DP-1 = off\noutput.DP-2.scale = 1.5\nrender.vrr = on\n", 1.5}, | |
| 22 | + | {"enable example before bind", "# Scale setting\n# output.DP-2.scale = 4/3\n\nbind.cmd+return = spawn-terminal\n", "# Scale setting\noutput.DP-2.scale = 1.25\n\nbind.cmd+return = spawn-terminal\n", 1.25}, | |
| 23 | + | {"insert beside output", "output.DP-2 = preferred\noutput.DP-1 = off\nrender.vrr = on\n", "output.DP-2 = preferred\noutput.DP-2.scale = 1.5\noutput.DP-1 = off\nrender.vrr = on\n", 1.5}, | |
| 24 | + | {"active beats commented example", "output.DP-2.scale = 2\n# output.DP-2.scale = 4/3\n", "output.DP-2.scale = 1.5\n# output.DP-2.scale = 4/3\n", 1.5}, | |
| 18 | 25 | {"keeps CRLF", "output.DP-2.scale = 2\r\nborder.width = 3\r\n", "output.DP-2.scale = 1.5\r\nborder.width = 3\r\n", 1.5}, | |
| 19 | 26 | {"comment with = and #", "output.DP-2.scale = 2 # was a=b #1\n", "output.DP-2.scale = 1.5 # was a=b #1\n", 1.5}, | |
| 20 | 27 | {"keeps spacing", "output.DP-2.scale=2\n", "output.DP-2.scale=4/3\n", 4.0 / 3}, | |
| 21 | 28 | {"last line wins", "output.DP-2.scale = 2\noutput.DP-2.scale = 3\n", "output.DP-2.scale = 2\noutput.DP-2.scale = 5/3\n", 5.0 / 3}, | |
| 22 | - | {"ignores comments and other outputs", "# output.DP-2.scale = 2\noutput.DP-1.scale = 2\n", "# output.DP-2.scale = 2\noutput.DP-1.scale = 2\noutput.DP-2.scale = 1\n", 1}, | |
| 29 | + | {"ignores other outputs", "# output.DP-1.scale = 2\noutput.DP-1.scale = 2\n", "# output.DP-1.scale = 2\noutput.DP-1.scale = 2\noutput.DP-2.scale = 1\n", 1}, | |
| 23 | 30 | } { | |
| 24 | 31 | t.Run(tc.name, func(t *testing.T) { | |
| 25 | 32 | path := filepath.Join(t.TempDir(), "neferwl", "config") | |
| @@ -46,11 +53,18 @@ func TestSaveOutputScale(t *testing.T) { | |||
| 46 | 53 | if err != nil { | |
| 47 | 54 | t.Fatal(err) | |
| 48 | 55 | } | |
| 56 | + | found := false | |
| 49 | 57 | for _, o := range cfg.Outputs { | |
| 50 | - | if o.Name == "DP-2" && o.Scale != tc.scale { | |
| 51 | - | t.Fatalf("parsed %v, want %v", o.Scale, tc.scale) | |
| 58 | + | if o.Name == "DP-2" { | |
| 59 | + | found = true | |
| 60 | + | if o.Scale != tc.scale { | |
| 61 | + | t.Fatalf("parsed %v, want %v", o.Scale, tc.scale) | |
| 62 | + | } | |
| 52 | 63 | } | |
| 53 | 64 | } | |
| 65 | + | if !found { | |
| 66 | + | t.Fatal("saved output missing from parsed config") | |
| 67 | + | } | |
| 54 | 68 | // Existing files keep their mode; new ones are private. | |
| 55 | 69 | { | |
| 56 | 70 | if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 { | |
| @@ -15,11 +15,18 @@ func TestSaveOutputScale(t *testing.T) { | |||||
| 15 | {"missing file", "", "output.DP-2.scale = 1.5\n", 1.5}, | 15 | {"missing file", "", "output.DP-2.scale = 1.5\n", 1.5}, | ||
| 16 | {"append", "# my config\nborder.width = 3", "# my config\nborder.width = 3\noutput.DP-2.scale = 1.5\n", 1.5}, | 16 | {"append", "# my config\nborder.width = 3", "# my config\nborder.width = 3\noutput.DP-2.scale = 1.5\n", 1.5}, | ||
| 17 | {"replace in place", "output.DP-2.scale = 2 # big\n# end\n", "output.DP-2.scale = 1.25 # big\n# end\n", 1.25}, | 17 | {"replace in place", "output.DP-2.scale = 2 # big\n# end\n", "output.DP-2.scale = 1.25 # big\n# end\n", 1.25}, | ||
| 18 | + | {"enable commented value", "# output.DP-2.scale = 4/3 # 4K display\nrender.vrr = on\n", "output.DP-2.scale = 1.5 # 4K display\nrender.vrr = on\n", 1.5}, | |||
| 19 | + | {"enable indented CRLF example", " # output.DP-2.scale = 2 # note\r\nrender.vrr = on\r\n", " output.DP-2.scale = 1.5 # note\r\nrender.vrr = on\r\n", 1.5}, | |||
| 20 | + | {"insert with same output group", "output.DP-2.hdr = off\noutput.DP-1 = off\n", "output.DP-2.hdr = off\noutput.DP-2.scale = 1.5\noutput.DP-1 = off\n", 1.5}, | |||
| 21 | + | {"insert with output group", "output.DP-1 = off\nrender.vrr = on\n", "output.DP-1 = off\noutput.DP-2.scale = 1.5\nrender.vrr = on\n", 1.5}, | |||
| 22 | + | {"enable example before bind", "# Scale setting\n# output.DP-2.scale = 4/3\n\nbind.cmd+return = spawn-terminal\n", "# Scale setting\noutput.DP-2.scale = 1.25\n\nbind.cmd+return = spawn-terminal\n", 1.25}, | |||
| 23 | + | {"insert beside output", "output.DP-2 = preferred\noutput.DP-1 = off\nrender.vrr = on\n", "output.DP-2 = preferred\noutput.DP-2.scale = 1.5\noutput.DP-1 = off\nrender.vrr = on\n", 1.5}, | |||
| 24 | + | {"active beats commented example", "output.DP-2.scale = 2\n# output.DP-2.scale = 4/3\n", "output.DP-2.scale = 1.5\n# output.DP-2.scale = 4/3\n", 1.5}, | |||
| 18 | {"keeps CRLF", "output.DP-2.scale = 2\r\nborder.width = 3\r\n", "output.DP-2.scale = 1.5\r\nborder.width = 3\r\n", 1.5}, | 25 | {"keeps CRLF", "output.DP-2.scale = 2\r\nborder.width = 3\r\n", "output.DP-2.scale = 1.5\r\nborder.width = 3\r\n", 1.5}, | ||
| 19 | {"comment with = and #", "output.DP-2.scale = 2 # was a=b #1\n", "output.DP-2.scale = 1.5 # was a=b #1\n", 1.5}, | 26 | {"comment with = and #", "output.DP-2.scale = 2 # was a=b #1\n", "output.DP-2.scale = 1.5 # was a=b #1\n", 1.5}, | ||
| 20 | {"keeps spacing", "output.DP-2.scale=2\n", "output.DP-2.scale=4/3\n", 4.0 / 3}, | 27 | {"keeps spacing", "output.DP-2.scale=2\n", "output.DP-2.scale=4/3\n", 4.0 / 3}, | ||
| 21 | {"last line wins", "output.DP-2.scale = 2\noutput.DP-2.scale = 3\n", "output.DP-2.scale = 2\noutput.DP-2.scale = 5/3\n", 5.0 / 3}, | 28 | {"last line wins", "output.DP-2.scale = 2\noutput.DP-2.scale = 3\n", "output.DP-2.scale = 2\noutput.DP-2.scale = 5/3\n", 5.0 / 3}, | ||
| 22 | - | {"ignores comments and other outputs", "# output.DP-2.scale = 2\noutput.DP-1.scale = 2\n", "# output.DP-2.scale = 2\noutput.DP-1.scale = 2\noutput.DP-2.scale = 1\n", 1}, | 29 | + | {"ignores other outputs", "# output.DP-1.scale = 2\noutput.DP-1.scale = 2\n", "# output.DP-1.scale = 2\noutput.DP-1.scale = 2\noutput.DP-2.scale = 1\n", 1}, |
| 23 | } { | 30 | } { | ||
| 24 | t.Run(tc.name, func(t *testing.T) { | 31 | t.Run(tc.name, func(t *testing.T) { | ||
| 25 | path := filepath.Join(t.TempDir(), "neferwl", "config") | 32 | path := filepath.Join(t.TempDir(), "neferwl", "config") | ||
| @@ -46,11 +53,18 @@ func TestSaveOutputScale(t *testing.T) { | |||||
| 46 | if err != nil { | 53 | if err != nil { | ||
| 47 | t.Fatal(err) | 54 | t.Fatal(err) | ||
| 48 | } | 55 | } | ||
| 56 | + | found := false | |||
| 49 | for _, o := range cfg.Outputs { | 57 | for _, o := range cfg.Outputs { | ||
| 50 | - | if o.Name == "DP-2" && o.Scale != tc.scale { | 58 | + | if o.Name == "DP-2" { |
| 51 | - | t.Fatalf("parsed %v, want %v", o.Scale, tc.scale) | 59 | + | found = true |
| 60 | + | if o.Scale != tc.scale { | |||
| 61 | + | t.Fatalf("parsed %v, want %v", o.Scale, tc.scale) | |||
| 62 | + | } | |||
| 52 | } | 63 | } | ||
| 53 | } | 64 | } | ||
| 65 | + | if !found { | |||
| 66 | + | t.Fatal("saved output missing from parsed config") | |||
| 67 | + | } | |||
| 54 | // Existing files keep their mode; new ones are private. | 68 | // Existing files keep their mode; new ones are private. | ||
| 55 | { | 69 | { | ||
| 56 | if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 { | 70 | if info, _ := os.Stat(path); info.Mode().Perm() != 0o600 { | ||