Repository: favonia/cloudflare-ddns
Pull request #886 (2024-08-23 19:08:07)

refactor: misc changes in preparation of Happy Eyeballs

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) Report
Attention: Patch coverage is `81.81818%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 95.00%. Comparing base [(`bf0361c`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/bf0361c85d449cdb703f9565f68dc8aaefb03323?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) to head [(`aae3204`)](https://app.codecov.io/gh/favonia/cloudflare-ddns/commit/aae32048445f57ad95da3dcb83d1311f7c478718?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).

| [Files](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Patch % | Lines |
|---|---|---|
| [cmd/ddns/ddns.go](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886?src=pr&el=tree&filepath=cmd%2Fddns%2Fddns.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#diff-Y21kL2RkbnMvZGRucy5nbw==) | 0.00% | [2 Missing :warning: ](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #886      +/-   ##
==========================================
+ Coverage   94.93%   95.00%   +0.07%     
==========================================
  Files          58       58              
  Lines        2506     2502       -4     
==========================================
- Hits         2379     2377       -2     
+ Misses        115      114       -1     
+ Partials       12       11       -1     
```

| [Flag](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | Coverage Δ | |
|---|---|---|
| [unittests](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia) | `95.00% <81.81%> (+0.07%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/favonia/cloudflare-ddns/pull/886?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=favonia).


Patch:
diff --git a/Dockerfile b/Dockerfile
index cac3e20..0580257 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,6 @@
 # We use cross-compilation because QEMU is slow.
-FROM --platform=${BUILDPLATFORM} golang:1.22.3-alpine3.18@sha256:d1a601b64de09e2fa38c95e55838961811d5ca11062a8f4230a5c434b3ae2a34 AS build
+FROM --platform=${BUILDPLATFORM} golang:1.23.0-alpine3.20@sha256:d0b31558e6b3e4cc59f6011d79905835108c919143ebecc58f35965bf79948f4 AS build
+
 ARG GIT_DESCRIBE
 ARG TARGETOS
 ARG TARGETARCH
diff --git a/cmd/ddns/ddns.go b/cmd/ddns/ddns.go
index aaf59ca..c0760e9 100644
--- a/cmd/ddns/ddns.go
+++ b/cmd/ddns/ddns.go
@@ -110,7 +110,7 @@ func realMain() int { //nolint:funlen
 	// We still want to keep the quiet mode extremely quiet for the single-run mode (UPDATE_CRON=@once),
 	// hence we are checking whether cron is enabled or not. (The single-run mode is defined as
 	// having the internal cron disabled.)
-	if c.UpdateCron != nil && ppfmt.Verbosity() <= pp.Quiet {
+	if c.UpdateCron != nil && ppfmt.IsShowing(pp.Verbose) {
 		ppfmt.Noticef(pp.EmojiMute, "Quiet mode enabled")
 	}
 
@@ -165,7 +165,7 @@ func realMain() int { //nolint:funlen
 
 	signaled:
 		// Wait for the next signal or the alarm, whichever comes first
-		if sig.ReportSignalsUntil(ppfmt, next) {
+		if sig.WaitForSignalsUntil(ppfmt, next) {
 			stopUpdating(ctx, ppfmt, c, s)
 			monitor.ExitStatusAll(ctx, ppfmt, c.Monitors, 0, "Stopped")
 			if c.UpdateCron != nil {
diff --git a/docs/DESIGN.markdown b/docs/DESIGN.markdown
index 80ee2fc..c5f1c49 100644
--- a/docs/DESIGN.markdown
+++ b/docs/DESIGN.markdown
@@ -2,7 +2,7 @@
 
 ## Principles and Priorities
 
-Be the 🌟 best DDNS updater 🌟 that [favonia](mailto:favonia+github@gmail.com) wants to use.
+Be the 🌟 best DDNS updater 🌟 that [favonia](mailto:favonia+github@gmail.com) (me) wants to use.
 
 1. Support all features [favonia](mailto:favonia+github@gmail.com) wants, including emojis.
 
@@ -25,9 +25,12 @@ Be the 🌟 best DDNS updater 🌟 that [favonia](mailto:favonia+github@gmail.co
 
 The source code follows the [standard Go project layout](https://github.com/golang-standards/project-layout), where `/cmd/` holds the command-line interface and `/internal/` holds the internal packages. The updater is factored into many internal packages, each in charged of a small part of the program logic. See the [Go Reference](https://pkg.go.dev/github.com/favonia/cloudflare-ddns/) for a detailed documentation of the code structure.
 
-### Logging Message Convention
+### Coding Convention
+
+Here is some arbitrary coding convention that I chose to follow. It may change in the future, but the whole codebase should be consistent with it at any time:
 
 1. Cloudflare IDs (zone IDs, DNS record IDs, etc.) are already designed to use only “very safe” characters and should not be quoted. The formatter `%s` should be used instead of `%q`.
+2. A variable name of type `map[..]...` is not in a plural form just because it is of type `map[...]...`. For example, a mapping from IP networks to detected IPs should be named `detectedIP` not `detectedIPs`.
 
 ## Network Security Threat Model
 
diff --git a/go.mod b/go.mod
index 602a491..7bbfda7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/favonia/cloudflare-ddns
 
-go 1.22.0
+go 1.23.0 // with patch version to satisfy CodeQL
 
 require (
 	github.com/cloudflare/cloudflare-go v0.102.0
diff --git a/internal/config/config_print.go b/internal/config/config_print.go
index 56919eb..955534e 100644
--- a/internal/config/config_print.go
+++ b/internal/config/config_print.go
@@ -43,7 +43,7 @@ func computeInverseMap[V comparable](m map[domain.Domain]V) ([]V, map[V][]domain
 
 // Print prints the Config on the screen.
 func (c *Config) Print(ppfmt pp.PP) {
-	if ppfmt.Verbosity() < pp.Info {
+	if !ppfmt.IsShowing(pp.Info) {
 		return
 	}
 
diff --git a/internal/config/config_read.go b/internal/config/config_read.go
index 476ecea..204c2c1 100644
--- a/internal/config/config_read.go
+++ b/internal/config/config_read.go
@@ -15,7 +15,7 @@ import (
 // - output-related ones (QUIET and EMOJI)
 // One should subsequently call [Config.Normalize] to restore invariants across fields.
 func (c *Config) ReadEnv(ppfmt pp.PP) bool {
-	if ppfmt.Verbosity() >= pp.Info {
+	if ppfmt.IsShowing(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Reading settings . . .")
 		ppfmt = ppfmt.Indent()
 	}
@@ -48,7 +48,7 @@ func (c *Config) ReadEnv(ppfmt pp.PP) bool {
 //
 //nolint:funlen
 func (c *Config) Normalize(ppfmt pp.PP) bool {
-	if ppfmt.Verbosity() >= pp.Info {
+	if ppfmt.IsShowing(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Checking settings . . .")
 		ppfmt = ppfmt.Indent()
 	}
@@ -121,7 +121,7 @@ func (c *Config) Normalize(ppfmt pp.PP) bool {
 		}
 	}
 
-	// Step 4: fill in proxiedMap
+	// Step 4: regenerate proxiedMap from [Config.Proxied]
 	proxiedMap := map[domain.Domain]bool{}
 	if len(activeDomainSet) > 0 {
 		proxiedPredicate, ok := domainexp.ParseExpression(ppfmt, "PROXIED", c.ProxiedTemplate)
diff --git a/internal/config/network_probe.go b/internal/config/network_probe.go
index 0dfecaf..f89de93 100644
--- a/internal/config/network_probe.go
+++ b/internal/config/network_probe.go
@@ -38,7 +38,7 @@ func (c *Config) ShouldWeUse1001Now(ctx context.Context, ppfmt pp.PP) bool {
 		return false // any answer would work
 	}
 
-	if ppfmt.Verbosity() >= pp.Info {
+	if ppfmt.IsShowing(pp.Info) {
 		ppfmt.Infof(pp.EmojiEnvVars, "Probing 1.1.1.1 and 1.0.0.1 . . .")
 		ppfmt = ppfmt.Indent()
 	}
diff --git a/internal/mocks/mock_pp.go b/internal/mocks/mock_pp.go
index f1ba5d7..d7e74f6 100644
--- a/internal/mocks/mock_pp.go
+++ b/internal/mocks/mock_pp.go
@@ -158,6 +158,44 @@ func (c *PPInfofCall) DoAndReturn(f func(pp.Emoji, string, ...any)) *PPInfofCall
 	return c
 }
 
+// IsShowing mocks base method.
+func (m *MockPP) IsShowing(arg0 pp.Verbosity) bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "IsShowing", arg0)
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// IsShowing indicates an expected call of IsShowing.
+func (mr *MockPPMockRecorder) IsShowing(arg0 any) *PPIsShowingCall {
+	mr.mock.ctrl.T.Helper()
+	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsShowing", reflect.TypeOf((*MockPP)(nil).IsShowing), arg0)
+	return &PPIsShowingCall{Call: call}
+}
+
+// PPIsShowingCall wrap *gomock.Call
+type PPIsShowingCall struct {
+	*gomock.Call
+}
+
+// Return rewrite *gomock.Call.Return
+func (c *PPIsShowingCall) Return(arg0 bool) *PPIsShowingCall {
+	c.Call = c.Call.Return(arg0)
+	return c
+}
+
+// Do rewrite *gomock.Call.Do
+func (c *PPIsShowingCall) Do(f func(pp.Verbosity) bool) *PPIsShowingCall {
+	c.Call = c.Call.Do(f)
+	return c
+}
+
+// DoAndReturn rewrite *gomock.Call.DoAndReturn
+func (c *PPIsShowingCall) DoAndReturn(f func(pp.Verbosity) bool) *PPIsShowingCall {
+	c.Call = c.Call.DoAndReturn(f)
+	return c
+}
+
 // Noticef mocks base method.
 func (m *MockPP) Noticef(arg0 pp.Emoji, arg1 string, arg2 ...any) {
 	m.ctrl.T.Helper()
@@ -234,41 +272,3 @@ func (c *PPSuppressHintCall) DoAndReturn(f func(pp.Hint)) *PPSuppressHintCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
-
-// Verbosity mocks base method.
-func (m *MockPP) Verbosity() pp.Verbosity {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Verbosity")
-	ret0, _ := ret[0].(pp.Verbosity)
-	return ret0
-}
-
-// Verbosity indicates an expected call of Verbosity.
-func (mr *MockPPMockRecorder) Verbosity() *PPVerbosityCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Verbosity", reflect.TypeOf((*MockPP)(nil).Verbosity))
-	return &PPVerbosityCall{Call: call}
-}
-
-// PPVerbosityCall wrap *gomock.Call
-type PPVerbosityCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *PPVerbosityCall) Return(arg0 pp.Verbosity) *PPVerbosityCall {
-	c.Call = c.Call.Return(arg0)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *PPVerbosityCall) Do(f func() pp.Verbosity) *PPVerbosityCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *PPVerbosityCall) DoAndReturn(f func() pp.Verbosity) *PPVerbosityCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
diff --git a/internal/pp/base.go b/internal/pp/base.go
index 6b7e3e9..8a14622 100644
--- a/internal/pp/base.go
+++ b/internal/pp/base.go
@@ -5,8 +5,8 @@ package pp
 
 // PP is the abstraction of a pretty printer.
 type PP interface {
-	// Verbosity returns the verbosity level.
-	Verbosity() Verbosity
+	// IsShowing checks whether a message of a certain level will be printed.
+	IsShowing(v Verbosity) bool
 
 	// Indent returns a new pretty-printer with more indentation.
 	Indent() PP
diff --git a/internal/pp/fmt.go b/internal/pp/fmt.go
index 1e8f1e8..ac3f9ab 100644
--- a/internal/pp/fmt.go
+++ b/internal/pp/fmt.go
@@ -42,9 +42,9 @@ func NewDefault(writer io.Writer) PP {
 	return New(writer, true, DefaultVerbosity)
 }
 
-// Verbosity returns the current verbosity level.
-func (f formatter) Verbosity() Verbosity {
-	return f.verbosity
+// IsShowing compares the internal verbosity level against the given level.
+func (f formatter) IsShowing(v Verbosity) bool {
+	return f.verbosity >= v
 }
 
 // Indent returns a new printer that indents the messages more than the input printer.
@@ -54,7 +54,7 @@ func (f formatter) Indent() PP {
 }
 
 func (f formatter) output(v Verbosity, emoji Emoji, msg string) {
-	if v > f.verbosity {
+	if !f.IsShowing(v) {
 		return
 	}
 
@@ -92,7 +92,7 @@ func (f formatter) SuppressHint(hint Hint) {
 	f.hintShown[hint] = true
 }
 
-// Hintf called [Infof] with the emoji [EmojiHint].
+// Hintf calls [Infof] with the emoji [EmojiHint].
 func (f formatter) Hintf(hint Hint, format string, args ...any) {
 	if !f.hintShown[hint] {
 		f.Infof(EmojiHint, format, args...)
diff --git a/internal/signal/signal.go b/internal/signal/signal.go
index 5f320d2..db314c1 100644
--- a/internal/signal/signal.go
+++ b/internal/signal/signal.go
@@ -34,20 +34,12 @@ func NotifyContext(ctx context.Context) (context.Context, context.CancelFunc) {
 	return signal.NotifyContext(ctx, Signals...)
 }
 
-// TearDown undoes what Setup does. This is only for testing.
-func (h Handle) TearDown() {
-	signal.Stop(h.channel)
-}
-
-// ReportSignalsUntil waits for a period of time. It returns true if it is interrupted by signals in [Signals].
-func (h Handle) ReportSignalsUntil(ppfmt pp.PP, t time.Time) bool {
+// WaitForSignalsUntil waits for a period of time. It returns true if it is interrupted by signals in [Signals].
+func (h Handle) WaitForSignalsUntil(ppfmt pp.PP, t time.Time) bool {
 	timer := time.NewTimer(time.Until(t))
 	for {
 		select {
 		case sig := <-h.channel:
-			if !timer.Stop() {
-				<-timer.C
-			}
 			ppfmt.Noticef(pp.EmojiSignal, "Caught signal: %v", sig)
 			return true
 		case <-timer.C:
diff --git a/internal/updater/updater.go b/internal/updater/updater.go
index de6d57d..282c1de 100644
--- a/internal/updater/updater.go
+++ b/internal/updater/updater.go
@@ -32,7 +32,7 @@ func detectIP(ctx context.Context, ppfmt pp.PP,
 
 	ip, ok := c.Provider[ipNet].GetIP(ctx, ppfmt, ipNet, use1001)
 	if ok {
-		ppfmt.Infof(pp.EmojiInternet, "Detected the %s address: %v", ipNet.Describe(), ip)
+		ppfmt.Infof(pp.EmojiInternet, "Detected the %s address %v", ipNet.Describe(), ip)
 		ppfmt.SuppressHint(getHintIDForDetection(ipNet))
 	} else {
 		ppfmt.Noticef(pp.EmojiError, "Failed to detect the %s address", ipNet.Describe())


Test patch:
diff --git a/internal/config/config_print_test.go b/internal/config/config_print_test.go
index 3ce7a12..d3ae178 100644
--- a/internal/config/config_print_test.go
+++ b/internal/config/config_print_test.go
@@ -28,7 +28,7 @@ func TestPrintDefault(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
 		mockPP.EXPECT().Indent().Return(mockPP),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
@@ -66,7 +66,7 @@ func TestPrintValues(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
 		mockPP.EXPECT().Indent().Return(mockPP),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
@@ -138,7 +138,7 @@ func TestPrintEmpty(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Current settings:"),
 		mockPP.EXPECT().Indent().Return(mockPP),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
@@ -171,7 +171,7 @@ func TestPrintHidden(t *testing.T) {
 	store(t, "TZ", "UTC")
 
 	mockPP := mocks.NewMockPP(mockCtrl)
-	mockPP.EXPECT().Verbosity().Return(pp.Notice)
+	mockPP.EXPECT().IsShowing(pp.Info).Return(false)
 
 	var cfg config.Config
 	cfg.Print(mockPP)
diff --git a/internal/config/config_read_test.go b/internal/config/config_read_test.go
index efbf0f9..b0d1c54 100644
--- a/internal/config/config_read_test.go
+++ b/internal/config/config_read_test.go
@@ -49,7 +49,7 @@ func TestReadEnvWithOnlyToken(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Reading settings . . ."),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
 		innerMockPP.EXPECT().Infof(pp.EmojiBullet, "Use default %s=%s", "IP4_PROVIDER", "none"),
@@ -76,7 +76,7 @@ func TestReadEnvEmpty(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Reading settings . . ."),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
 		innerMockPP.EXPECT().Noticef(pp.EmojiUserError, "Needs either CF_API_TOKEN or CF_API_TOKEN_FILE"),
@@ -104,7 +104,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, "Nothing was specified in DOMAINS, IP4_DOMAINS, IP6_DOMAINS, or WAF_LISTS"),
@@ -122,7 +122,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, "UPDATE_ON_START=false is incompatible with UPDATE_CRON=@once"),
@@ -141,7 +141,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, "DELETE_ON_STOP=true will immediately delete all domains and WAF lists when UPDATE_CRON=@once"), //nolint:lll
@@ -164,7 +164,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError,
@@ -201,7 +201,7 @@ func TestNormalize(t *testing.T) {
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserWarning,
@@ -224,7 +224,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserWarning,
@@ -266,7 +266,7 @@ func TestNormalize(t *testing.T) {
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserWarning,
@@ -302,7 +302,7 @@ func TestNormalize(t *testing.T) {
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserWarning,
@@ -349,7 +349,7 @@ func TestNormalize(t *testing.T) {
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserWarning,
@@ -387,7 +387,7 @@ func TestNormalize(t *testing.T) {
 			},
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 				)
@@ -408,7 +408,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, "%s (%q) is not a boolean expression: got unexpected token %q", keyProxied, `range`, `range`), //nolint:lll
@@ -430,7 +430,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, "%s (%q) is not a boolean expression: got unexpected token %q", keyProxied, `999`, `999`), //nolint:lll
@@ -452,7 +452,7 @@ func TestNormalize(t *testing.T) {
 			expected: nil,
 			prepareMockPP: func(m *mocks.MockPP) {
 				gomock.InOrder(
-					m.EXPECT().Verbosity().Return(pp.Info),
+					m.EXPECT().IsShowing(pp.Info).Return(true),
 					m.EXPECT().Infof(pp.EmojiEnvVars, "Checking settings . . ."),
 					m.EXPECT().Indent().Return(m),
 					m.EXPECT().Noticef(pp.EmojiUserError, `%s (%q) is missing %q at the end`, keyProxied, `is(12345`, ")"),
diff --git a/internal/config/network_probe_test.go b/internal/config/network_probe_test.go
index a279f8f..c11eea3 100644
--- a/internal/config/network_probe_test.go
+++ b/internal/config/network_probe_test.go
@@ -41,7 +41,7 @@ func TestProbeCloudflareIPs(t *testing.T) {
 	mockPP := mocks.NewMockPP(mockCtrl)
 	innerMockPP := mocks.NewMockPP(mockCtrl)
 	gomock.InOrder(
-		mockPP.EXPECT().Verbosity().Return(pp.Info),
+		mockPP.EXPECT().IsShowing(pp.Info).Return(true),
 		mockPP.EXPECT().Infof(pp.EmojiEnvVars, "Probing 1.1.1.1 and 1.0.0.1 . . ."),
 		mockPP.EXPECT().Indent().Return(innerMockPP),
 		innerMockPP.EXPECT().Infof(pp.EmojiGood, "1.1.1.1 is working. Great!"),
diff --git a/internal/pp/fmt_test.go b/internal/pp/fmt_test.go
index 61b8479..a76762a 100644
--- a/internal/pp/fmt_test.go
+++ b/internal/pp/fmt_test.go
@@ -1,7 +1,6 @@
 package pp_test
 
 import (
-	"io"
 	"strings"
 	"testing"
 
@@ -10,10 +9,26 @@ import (
 	"github.com/favonia/cloudflare-ddns/internal/pp"
 )
 
-func TestVerbosity(t *testing.T) {
+func TestIsShowing(t *testing.T) {
 	t.Parallel()
 
-	require.Equal(t, pp.Verbosity(123), pp.New(io.Discard, true, 123).Verbosity())
+	for name, tc := range map[string]struct {
+		set      pp.Verbosity
+		test     pp.Verbosity
+		expected bool
+	}{
+		"info/notice": {pp.Info, pp.Notice, true},
+		"notice/info": {pp.Notice, pp.Info, false},
+	} {
+		t.Run(name, func(t *testing.T) {
+			t.Parallel()
+
+			var buf strings.Builder
+			fmt := pp.New(&buf, true, tc.set)
+
+			require.Equal(t, tc.expected, fmt.IsShowing(tc.test))
+		})
+	}
 }
 
 func TestIndent(t *testing.T) {
diff --git a/internal/signal/signal_test.go b/internal/signal/signal_test.go
index c343566..c6327fb 100644
--- a/internal/signal/signal_test.go
+++ b/internal/signal/signal_test.go
@@ -16,7 +16,7 @@ import (
 )
 
 //nolint:paralleltest //signals are global
-func TestReportSignalUntil(t *testing.T) {
+func TestWaitForSignalsUntil(t *testing.T) {
 	for name, tc := range map[string]struct {
 		alarmDelay    time.Duration
 		signalDelay   time.Duration
@@ -58,9 +58,8 @@ func TestReportSignalUntil(t *testing.T) {
 			sig := signal.Setup()
 			go signalSelf()
 			target := time.Now().Add(tc.alarmDelay)
-			res := sig.ReportSignalsUntil(mockPP, target)
+			res := sig.WaitForSignalsUntil(mockPP, target)
 			<-done
-			sig.TearDown()
 
 			require.Equal(t, tc.expected, res)
 		})
diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go
index 3879f71..657d36c 100644
--- a/internal/updater/updater_test.go
+++ b/internal/updater/updater_test.go
@@ -96,7 +96,7 @@ func TestUpdateIPsMultiple(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello1"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseUpdated),
@@ -131,7 +131,7 @@ func TestUpdateIPsMultiple(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello1"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseUpdated),
@@ -308,7 +308,7 @@ func TestUpdateIPsUninitializedProxied(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					p.EXPECT().Noticef(pp.EmojiImpossible,
 						"Proxied[%s] not initialized; this should not happen; please report this at %s",
@@ -378,7 +378,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -398,7 +398,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseFailed),
@@ -418,7 +418,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseUpdated),
@@ -438,7 +438,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseFailed),
@@ -453,12 +453,12 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -476,12 +476,12 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseFailed),
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -499,12 +499,12 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseFailed),
@@ -522,12 +522,12 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) { //nolint:dupl
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -548,7 +548,7 @@ func TestUpdateIPs(t *testing.T) {
 					p.EXPECT().Noticef(pp.EmojiError, "Failed to detect the %s address", "IPv4"),
 					p.EXPECT().Hintf(pp.HintIP4DetectionFails, "If your network does not support IPv4, you can disable it with IP4_PROVIDER=none"), //nolint:lll
 					pv[ipnet.IP6].EXPECT().GetIP(gomock.Any(), p, ipnet.IP6, true).Return(ip6, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv6", ip6),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv6", ip6),
 					p.EXPECT().SuppressHint(pp.HintIP6DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP6, domain.FQDN("ip6.hello"), ip6, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -565,7 +565,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),
@@ -623,7 +623,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						DoAndReturn(
@@ -645,7 +645,7 @@ func TestUpdateIPs(t *testing.T) {
 			func(p *mocks.MockPP, pv mockProviders, s *mocks.MockSetter) {
 				gomock.InOrder(
 					pv[ipnet.IP4].EXPECT().GetIP(gomock.Any(), p, ipnet.IP4, true).Return(ip4, true),
-					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address: %v", "IPv4", ip4),
+					p.EXPECT().Infof(pp.EmojiInternet, "Detected the %s address %v", "IPv4", ip4),
 					p.EXPECT().SuppressHint(pp.HintIP4DetectionFails),
 					s.EXPECT().Set(gomock.Any(), p, ipnet.IP4, domain.FQDN("ip4.hello"), ip4, api.TTLAuto, false, recordComment).
 						Return(setter.ResponseNoop),