Repository: gohugoio/hugo (language: go)

Issue:
Data race in TestRelated and TestPermalinkExpansionSectionsRepeated on Windows
The following data race occurred during the [CI run](https://github.com/gohugoio/hugo/actions/runs/4302554709/jobs/7501194890#step:17:289) on Go 1.19, Windows:

[The full log](https://github.com/gohugoio/hugo/files/10861218/data_race_test_go119_windows.log)

```
==================
WARNING: DATA RACE
Write at 0x000142582750 by goroutine 103:
  github.com/gohugoio/hugo/related.(*IndexConfig).incrNumDocs()
      D:/a/hugo/hugo/related/inverted_index.go:152 +0x484
  github.com/gohugoio/hugo/related.(*InvertedIndex).Add()
      D:/a/hugo/hugo/related/inverted_index.go:255 +0x43d
  github.com/gohugoio/hugo/resources/page.(*RelatedDocsHandler).getOrCreateIndex()
      D:/a/hugo/hugo/resources/page/pages_related.go:232 +0xa1d
  github.com/gohugoio/hugo/resources/page.Pages.withInvertedIndex()
      D:/a/hugo/hugo/resources/page/pages_related.go:136 +0xe5
  github.com/gohugoio/hugo/resources/page.Pages.search()
      D:/a/hugo/hugo/resources/page/pages_related.go:119 +0xfa
  github.com/gohugoio/hugo/resources/page.Pages.Related()
      D:/a/hugo/hugo/resources/page/pages_related.go:73 +0x384
  github.com/gohugoio/hugo/resources/page.TestRelated()
      D:/a/hugo/hugo/resources/page/pages_related_test.go:59 +0xc97
  testing.tRunner()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1446 +0x216
  testing.(*T).Run.func1()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1493 +0x47

Previous read at 0x000142582750 by goroutine 20:
  [failed to restore the stack]

Goroutine 103 (running) created at:
  testing.(*T).Run()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1493 +0x75d
  testing.runTests.func1()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1846 +0x99
  testing.tRunner()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1446 +0x216
  testing.runTests()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1844 +0x7ec
  testing.(*M).Run()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1726 +0xa84
  main.main()
      _testmain.go:177 +0x2e9

Goroutine 20 (running) created at:
  testing.(*T).Run()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1493 +0x75d
  testing.runTests.func1()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1846 +0x99
  testing.tRunner()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1446 +0x216
  testing.runTests()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1844 +0x7ec
  testing.(*M).Run()
      C:/hostedtoolcache/windows/go/1.19.6/x64/src/testing/testing.go:1726 +0xa84
  main.main()
      _testmain.go:177 +0x2e9
==================
--- FAIL: TestRelated (0.01s)
    testing.go:1[319](https://github.com/gohugoio/hugo/actions/runs/4302554709/jobs/7501194890#step:17:320): race detected during execution of test
--- FAIL: TestPermalinkExpansionSectionsRepeated (0.40s)
    testing.go:1319: race detected during execution of test
FAIL
FAIL	github.com/gohugoio/hugo/resources/page	1.432s
```


Discussion / hints:

Fix patch:
diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go
index 63d939ef690..8723c4bdcd2 100644
--- a/cache/filecache/filecache.go
+++ b/cache/filecache/filecache.go
@@ -17,7 +17,6 @@ import (
 	"bytes"
 	"errors"
 	"io"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -207,7 +206,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
 
 	if r := c.getOrRemove(id); r != nil {
 		defer r.Close()
-		b, err := ioutil.ReadAll(r)
+		b, err := io.ReadAll(r)
 		return info, b, err
 	}
 
@@ -242,7 +241,7 @@ func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
 
 	if r := c.getOrRemove(id); r != nil {
 		defer r.Close()
-		b, err := ioutil.ReadAll(r)
+		b, err := io.ReadAll(r)
 		return info, b, err
 	}
 
@@ -314,7 +313,7 @@ func (c *Cache) getString(id string) string {
 	}
 	defer f.Close()
 
-	b, _ := ioutil.ReadAll(f)
+	b, _ := io.ReadAll(f)
 	return string(b)
 }
 
diff --git a/commands/commandeer.go b/commands/commandeer.go
index 80d4c55d0b3..b77e9e90882 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -16,7 +16,7 @@ package commands
 import (
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net"
 	"os"
 	"path/filepath"
@@ -201,7 +201,7 @@ func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuild
 		rebuildDebouncer = debounce.New(4 * time.Second)
 	}
 
-	out := ioutil.Discard
+	out := io.Discard
 	if !h.quiet {
 		out = os.Stdout
 	}
@@ -221,7 +221,7 @@ func newCommandeer(mustHaveConfigFile, failOnInitErr, running bool, h *hugoBuild
 		running:            running,
 
 		// This will be replaced later, but we need something to log to before the configuration is read.
-		logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, ioutil.Discard, running),
+		logger: loggers.NewLogger(jww.LevelWarn, jww.LevelError, out, io.Discard, running),
 	}
 
 	return c, c.loadConfig()
diff --git a/commands/hugo.go b/commands/hugo.go
index d2d99c7fabc..2fa08ec21e5 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -18,7 +18,7 @@ package commands
 import (
 	"context"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"os"
 	"os/signal"
 	"path/filepath"
@@ -138,10 +138,10 @@ func initializeConfig(mustHaveConfigFile, failOnInitErr, running bool,
 
 func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) {
 	var (
-		logHandle       = ioutil.Discard
+		logHandle       = io.Discard
 		logThreshold    = jww.LevelWarn
 		logFile         = cfg.GetString("logFile")
-		outHandle       = ioutil.Discard
+		outHandle       = io.Discard
 		stdoutThreshold = jww.LevelWarn
 	)
 
@@ -157,7 +157,7 @@ func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) {
 				return nil, newSystemError("Failed to open log file:", logFile, err)
 			}
 		} else {
-			logHandle, err = ioutil.TempFile("", "hugo")
+			logHandle, err = os.CreateTemp("", "hugo")
 			if err != nil {
 				return nil, newSystemError(err)
 			}
diff --git a/commands/import_jekyll.go b/commands/import_jekyll.go
index 91d5c69fe54..93991121d61 100644
--- a/commands/import_jekyll.go
+++ b/commands/import_jekyll.go
@@ -17,7 +17,7 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -164,7 +164,7 @@ func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
 func (i *importCmd) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
 	postDirs := make(map[string]bool)
 	hasAnyPost := false
-	if entries, err := ioutil.ReadDir(jekyllRoot); err == nil {
+	if entries, err := os.ReadDir(jekyllRoot); err == nil {
 		for _, entry := range entries {
 			if entry.IsDir() {
 				subDir := filepath.Join(jekyllRoot, entry.Name())
@@ -186,7 +186,7 @@ func (i *importCmd) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool)
 		return true, !isEmpty
 	}
 
-	if entries, err := ioutil.ReadDir(dir); err == nil {
+	if entries, err := os.ReadDir(dir); err == nil {
 		for _, entry := range entries {
 			if entry.IsDir() {
 				subDir := filepath.Join(dir, entry.Name())
@@ -247,7 +247,7 @@ func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]
 
 	defer f.Close()
 
-	b, err := ioutil.ReadAll(f)
+	b, err := io.ReadAll(f)
 	if err != nil {
 		return nil
 	}
@@ -310,7 +310,7 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
 	if err != nil {
 		return err
 	}
-	entries, err := ioutil.ReadDir(jekyllRoot)
+	entries, err := os.ReadDir(jekyllRoot)
 	if err != nil {
 		return err
 	}
@@ -386,7 +386,7 @@ func convertJekyllPost(path, relPath, targetDir string, draft bool) error {
 	targetParentDir := filepath.Dir(targetFile)
 	os.MkdirAll(targetParentDir, 0777)
 
-	contentBytes, err := ioutil.ReadFile(path)
+	contentBytes, err := os.ReadFile(path)
 	if err != nil {
 		jww.ERROR.Println("Read file error:", path)
 		return err
diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go
index 18c21e51b2c..c7e2d2f06f5 100644
--- a/common/herrors/error_locator.go
+++ b/common/herrors/error_locator.go
@@ -16,7 +16,6 @@ package herrors
 
 import (
 	"io"
-	"io/ioutil"
 	"path/filepath"
 	"strings"
 
@@ -114,7 +113,7 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
 
 	ectx := &ErrorContext{LinesPos: -1, Position: text.Position{Offset: -1}}
 
-	b, err := ioutil.ReadAll(r)
+	b, err := io.ReadAll(r)
 	if err != nil {
 		return ectx
 	}
diff --git a/common/hugio/writers.go b/common/hugio/writers.go
index e600c1ebb0b..729b2171573 100644
--- a/common/hugio/writers.go
+++ b/common/hugio/writers.go
@@ -15,7 +15,6 @@ package hugio
 
 import (
 	"io"
-	"io/ioutil"
 )
 
 // As implemented by strings.Builder.
@@ -63,7 +62,7 @@ func ToWriteCloser(w io.Writer) io.WriteCloser {
 		io.Closer
 	}{
 		w,
-		ioutil.NopCloser(nil),
+		io.NopCloser(nil),
 	}
 }
 
@@ -79,6 +78,6 @@ func ToReadCloser(r io.Reader) io.ReadCloser {
 		io.Closer
 	}{
 		r,
-		ioutil.NopCloser(nil),
+		io.NopCloser(nil),
 	}
 }
diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go
index 308635fe9d4..2c331352a6b 100644
--- a/common/loggers/loggers.go
+++ b/common/loggers/loggers.go
@@ -17,7 +17,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"log"
 	"os"
 	"regexp"
@@ -92,7 +91,7 @@ type logger struct {
 	*jww.Notepad
 
 	// The writer that represents stdout.
-	// Will be ioutil.Discard when in quiet mode.
+	// Will be io.Discard when in quiet mode.
 	out io.Writer
 
 	logCounters *LogCounters
@@ -232,12 +231,12 @@ func NewErrorLogger() Logger {
 
 // NewBasicLogger creates a new basic logger writing to Stdout.
 func NewBasicLogger(t jww.Threshold) Logger {
-	return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false)
+	return newLogger(t, jww.LevelError, os.Stdout, io.Discard, false)
 }
 
 // NewBasicLoggerForWriter creates a new basic logger writing to w.
 func NewBasicLoggerForWriter(t jww.Threshold, w io.Writer) Logger {
-	return newLogger(t, jww.LevelError, w, ioutil.Discard, false)
+	return newLogger(t, jww.LevelError, w, io.Discard, false)
 }
 
 // RemoveANSIColours removes all ANSI colours from the given string.
@@ -291,7 +290,7 @@ func InitGlobalLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, lo
 
 func getLogWriters(outHandle, logHandle io.Writer) (io.Writer, io.Writer) {
 	isTerm := terminal.PrintANSIColors(os.Stdout)
-	if logHandle != ioutil.Discard && isTerm {
+	if logHandle != io.Discard && isTerm {
 		// Remove any Ansi coloring from log output
 		logHandle = ansiCleaner{w: logHandle}
 	}
diff --git a/deploy/deploy.go b/deploy/deploy.go
index f0a4e017837..5675d7aa04f 100644
--- a/deploy/deploy.go
+++ b/deploy/deploy.go
@@ -24,7 +24,6 @@ import (
 	"encoding/hex"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"mime"
 	"os"
 	"path/filepath"
@@ -403,7 +402,7 @@ func (lf *localFile) Reader() (io.ReadCloser, error) {
 		// We've got the gzipped contents cached in gzipped.
 		// Note: we can't use lf.gzipped directly as a Reader, since we it discards
 		// data after it is read, and we may read it more than once.
-		return ioutil.NopCloser(bytes.NewReader(lf.gzipped.Bytes())), nil
+		return io.NopCloser(bytes.NewReader(lf.gzipped.Bytes())), nil
 	}
 	// Not expected to fail since we did it successfully earlier in newLocalFile,
 	// but could happen due to changes in the underlying filesystem.
diff --git a/magefile.go b/magefile.go
index c8f2a048e9c..bd9c5b830c5 100644
--- a/magefile.go
+++ b/magefile.go
@@ -7,7 +7,6 @@ import (
 	"bytes"
 	"errors"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -298,7 +297,7 @@ func TestCoverHTML() error {
 		if err := sh.Run(goexe, "test", "-coverprofile="+cover, "-covermode=count", pkg); err != nil {
 			return err
 		}
-		b, err := ioutil.ReadFile(cover)
+		b, err := os.ReadFile(cover)
 		if err != nil {
 			if os.IsNotExist(err) {
 				continue
diff --git a/modules/client.go b/modules/client.go
index 953391e5934..1fff787d1c9 100644
--- a/modules/client.go
+++ b/modules/client.go
@@ -20,7 +20,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -443,7 +442,7 @@ func (c *Client) Clean(pattern string) error {
 }
 
 func (c *Client) runVerify() error {
-	return c.runGo(context.Background(), ioutil.Discard, "mod", "verify")
+	return c.runGo(context.Background(), io.Discard, "mod", "verify")
 }
 
 func isProbablyModule(path string) bool {
@@ -458,7 +457,7 @@ func (c *Client) listGoMods() (goModules, error) {
 	downloadModules := func(modules ...string) error {
 		args := []string{"mod", "download"}
 		args = append(args, modules...)
-		out := ioutil.Discard
+		out := io.Discard
 		err := c.runGo(context.Background(), out, args...)
 		if err != nil {
 			return fmt.Errorf("failed to download modules: %w", err)
diff --git a/parser/pageparser/pageparser.go b/parser/pageparser/pageparser.go
index 7687479078c..8d4c757af24 100644
--- a/parser/pageparser/pageparser.go
+++ b/parser/pageparser/pageparser.go
@@ -18,7 +18,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"regexp"
 	"strings"
 
@@ -100,7 +99,7 @@ func ParseMain(r io.Reader, cfg Config) (Result, error) {
 }
 
 func parseSection(r io.Reader, cfg Config, start stateFunc) (Result, error) {
-	b, err := ioutil.ReadAll(r)
+	b, err := io.ReadAll(r)
 	if err != nil {
 		return nil, fmt.Errorf("failed to read page content: %w", err)
 	}
diff --git a/related/inverted_index.go b/related/inverted_index.go
index 967855133dc..ae894e5220d 100644
--- a/related/inverted_index.go
+++ b/related/inverted_index.go
@@ -143,13 +143,6 @@ type IndexConfig struct {
 	// Will lower case all string values in and queries tothis index.
 	// May get better accurate results, but at a slight performance cost.
 	ToLower bool
-
-	// Counts the number of documents in the index.
-	numDocs int
-}
-
-func (cfg *IndexConfig) incrNumDocs() {
-	cfg.numDocs++
 }
 
 // Document is the interface an indexable document in Hugo must fulfill.
@@ -178,6 +171,8 @@ type FragmentProvider interface {
 type InvertedIndex struct {
 	cfg   Config
 	index map[string]map[Keyword][]Document
+	// Counts the number of documents added to each index.
+	indexDocCount map[string]int
 
 	minWeight int
 	maxWeight int
@@ -199,7 +194,7 @@ func (idx *InvertedIndex) getIndexCfg(name string) (IndexConfig, bool) {
 // NewInvertedIndex creates a new InvertedIndex.
 // Documents to index must be added in Add.
 func NewInvertedIndex(cfg Config) *InvertedIndex {
-	idx := &InvertedIndex{index: make(map[string]map[Keyword][]Document), cfg: cfg}
+	idx := &InvertedIndex{index: make(map[string]map[Keyword][]Document), indexDocCount: make(map[string]int), cfg: cfg}
 	for _, conf := range cfg.Indices {
 		idx.index[conf.Name] = make(map[Keyword][]Document)
 		if conf.Weight < idx.minWeight {
@@ -221,7 +216,7 @@ func (idx *InvertedIndex) Add(ctx context.Context, docs ...Document) error {
 		panic("index is finalized")
 	}
 	var err error
-	for i, config := range idx.cfg.Indices {
+	for _, config := range idx.cfg.Indices {
 		if config.Weight == 0 {
 			// Disabled
 			continue
@@ -251,8 +246,7 @@ func (idx *InvertedIndex) Add(ctx context.Context, docs ...Document) error {
 			}
 
 			if added {
-				c := &idx.cfg.Indices[i]
-				(*c).incrNumDocs()
+				idx.indexDocCount[config.Name]++
 			}
 		}
 	}
@@ -270,12 +264,12 @@ func (idx *InvertedIndex) Finalize(ctx context.Context) error {
 			continue
 		}
 		setm := idx.index[config.Name]
-		numDocs := config.numDocs
-		if numDocs == 0 {
+		if idx.indexDocCount[config.Name] == 0 {
 			continue
 		}
 
 		// Remove high cardinality terms.
+		numDocs := idx.indexDocCount[config.Name]
 		for k, v := range setm {
 			percentageWithKeyword := int(math.Ceil(float64(len(v)) / float64(numDocs) * 100))
 			if percentageWithKeyword > config.CardinalityThreshold {
diff --git a/resources/image.go b/resources/image.go
index 547b3305f26..6deb0dfe70e 100644
--- a/resources/image.go
+++ b/resources/image.go
@@ -23,7 +23,6 @@ import (
 	_ "image/gif"
 	_ "image/png"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -92,7 +91,7 @@ func (i *imageResource) getExif() *exif.ExifInfo {
 
 		read := func(info filecache.ItemInfo, r io.ReadSeeker) error {
 			meta := &imageMeta{}
-			data, err := ioutil.ReadAll(r)
+			data, err := io.ReadAll(r)
 			if err != nil {
 				return err
 			}
diff --git a/resources/resource.go b/resources/resource.go
index 7ccc5da39de..94016154a02 100644
--- a/resources/resource.go
+++ b/resources/resource.go
@@ -17,7 +17,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -370,7 +369,7 @@ func (l *genericResource) initContent() error {
 		defer r.Close()
 
 		var b []byte
-		b, err = ioutil.ReadAll(r)
+		b, err = io.ReadAll(r)
 		if err != nil {
 			return
 		}
diff --git a/resources/resource_factories/create/remote.go b/resources/resource_factories/create/remote.go
index 8f1707ed0ef..7310569f9a8 100644
--- a/resources/resource_factories/create/remote.go
+++ b/resources/resource_factories/create/remote.go
@@ -18,7 +18,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"mime"
 	"net/http"
 	"net/http/httputil"
@@ -48,7 +47,7 @@ type HTTPError struct {
 func responseToData(res *http.Response, readBody bool) map[string]any {
 	var body []byte
 	if readBody {
-		body, _ = ioutil.ReadAll(res.Body)
+		body, _ = io.ReadAll(res.Body)
 	}
 
 	m := map[string]any{
@@ -157,7 +156,7 @@ func (c *Client) FromRemote(uri string, optionsm map[string]any) (resource.Resou
 	// A response to a HEAD method should not have a body. If it has one anyway, that body must be ignored.
 	// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
 	if !isHeadMethod && res.Body != nil {
-		body, err = ioutil.ReadAll(res.Body)
+		body, err = io.ReadAll(res.Body)
 		if err != nil {
 			return nil, fmt.Errorf("failed to read remote resource %q: %w", uri, err)
 		}
diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go
index 9a9110f62bd..89d74d9ed28 100644
--- a/resources/resource_transformers/babel/babel.go
+++ b/resources/resource_transformers/babel/babel.go
@@ -17,7 +17,6 @@ import (
 	"bytes"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -162,7 +161,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
 	// Create compile into a real temp file:
 	// 1. separate stdout/stderr messages from babel (https://github.com/gohugoio/hugo/issues/8136)
 	// 2. allow generation and retrieval of external source map.
-	compileOutput, err := ioutil.TempFile("", "compileOut-*.js")
+	compileOutput, err := os.CreateTemp("", "compileOut-*.js")
 	if err != nil {
 		return err
 	}
@@ -206,7 +205,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
 		return fmt.Errorf(errBuf.String()+": %w", err)
 	}
 
-	content, err := ioutil.ReadAll(compileOutput)
+	content, err := io.ReadAll(compileOutput)
 	if err != nil {
 		return err
 	}
@@ -214,7 +213,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
 	mapFile := compileOutput.Name() + ".map"
 	if _, err := os.Stat(mapFile); err == nil {
 		defer os.Remove(mapFile)
-		sourceMap, err := ioutil.ReadFile(mapFile)
+		sourceMap, err := os.ReadFile(mapFile)
 		if err != nil {
 			return err
 		}
diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go
index 23e28f6754d..34bc2cc1291 100644
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -15,7 +15,7 @@ package js
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 	"os"
 	"path"
 	"path/filepath"
@@ -77,7 +77,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
 		ctx.ReplaceOutPathExtension(".js")
 	}
 
-	src, err := ioutil.ReadAll(ctx.From)
+	src, err := io.ReadAll(ctx.From)
 	if err != nil {
 		return err
 	}
@@ -98,7 +98,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
 	}
 
 	if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
-		buildOptions.Outdir, err = ioutil.TempDir(os.TempDir(), "compileOutput")
+		buildOptions.Outdir, err = os.MkdirTemp(os.TempDir(), "compileOutput")
 		if err != nil {
 			return err
 		}
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
index 71ae0457307..ff5cca458ed 100644
--- a/resources/resource_transformers/js/options.go
+++ b/resources/resource_transformers/js/options.go
@@ -16,7 +16,7 @@ package js
 import (
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"strings"
 
@@ -260,7 +260,7 @@ func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
 				})
 			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
 				func(args api.OnLoadArgs) (api.OnLoadResult, error) {
-					b, err := ioutil.ReadFile(args.Path)
+					b, err := os.ReadFile(args.Path)
 					if err != nil {
 						return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
 					}
diff --git a/resources/resource_transformers/postcss/postcss.go b/resources/resource_transformers/postcss/postcss.go
index eab52b8c509..b4234bcf826 100644
--- a/resources/resource_transformers/postcss/postcss.go
+++ b/resources/resource_transformers/postcss/postcss.go
@@ -19,7 +19,6 @@ import (
 	"encoding/hex"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"path"
 	"path/filepath"
 	"regexp"
@@ -365,7 +364,7 @@ func (imp *importResolver) importRecursive(
 func (imp *importResolver) resolve() (io.Reader, error) {
 	const importIdentifier = "@import"
 
-	content, err := ioutil.ReadAll(imp.r)
+	content, err := io.ReadAll(imp.r)
 	if err != nil {
 		return nil, err
 	}
diff --git a/scripts/fork_go_templates/main.go b/scripts/fork_go_templates/main.go
index c22ecd92c9d..bdc190a697c 100644
--- a/scripts/fork_go_templates/main.go
+++ b/scripts/fork_go_templates/main.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"fmt"
-	"io/ioutil"
 	"log"
 	"os"
 	"path/filepath"
@@ -186,7 +185,7 @@ func doWithGoFiles(dir string,
 			return nil
 		}
 
-		data, err := ioutil.ReadFile(path)
+		data, err := os.ReadFile(path)
 		must(err)
 		f, err := os.Create(path)
 		must(err)
diff --git a/tpl/data/resources.go b/tpl/data/resources.go
index 0470faf51ae..d7c1a157445 100644
--- a/tpl/data/resources.go
+++ b/tpl/data/resources.go
@@ -16,7 +16,7 @@ package data
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"net/http"
 	"net/url"
 	"path/filepath"
@@ -62,7 +62,7 @@ func (ns *Namespace) getRemote(cache *filecache.Cache, unmarshal func([]byte) (b
 			}
 
 			var b []byte
-			b, err = ioutil.ReadAll(res.Body)
+			b, err = io.ReadAll(res.Body)
 			if err != nil {
 				return nil, err
 			}
diff --git a/tpl/openapi/openapi3/openapi3.go b/tpl/openapi/openapi3/openapi3.go
index 31d56289ec8..74c731f025d 100644
--- a/tpl/openapi/openapi3/openapi3.go
+++ b/tpl/openapi/openapi3/openapi3.go
@@ -16,7 +16,7 @@ package openapi3
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 
 	gyaml "github.com/ghodss/yaml"
 
@@ -74,7 +74,7 @@ func (ns *Namespace) Unmarshal(r resource.UnmarshableResource) (*OpenAPIDocument
 		}
 		defer reader.Close()
 
-		b, err := ioutil.ReadAll(reader)
+		b, err := io.ReadAll(reader)
 		if err != nil {
 			return nil, err
 		}
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index 3b08604b704..d9a826aa41b 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -20,7 +20,6 @@ import (
 	"fmt"
 	"html/template"
 	"io"
-	"io/ioutil"
 	"strings"
 	"time"
 
@@ -193,7 +192,7 @@ func (ns *Namespace) include(ctx context.Context, name string, dataList ...any)
 		}
 
 		// We don't care about any template output.
-		w = ioutil.Discard
+		w = io.Discard
 	} else {
 		b := bp.GetBuffer()
 		defer bp.PutBuffer(b)
diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go
index 340235fa444..f5ff635850a 100644
--- a/tpl/transform/unmarshal.go
+++ b/tpl/transform/unmarshal.go
@@ -15,7 +15,7 @@ package transform
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 	"strings"
 
 	"github.com/gohugoio/hugo/resources/resource"
@@ -83,7 +83,7 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) {
 			}
 			defer reader.Close()
 
-			b, err := ioutil.ReadAll(reader)
+			b, err := io.ReadAll(reader)
 			if err != nil {
 				return nil, err
 			}
diff --git a/transform/chain.go b/transform/chain.go
index a5f042d9666..d4e7b62232a 100644
--- a/transform/chain.go
+++ b/transform/chain.go
@@ -16,7 +16,7 @@ package transform
 import (
 	"bytes"
 	"io"
-	"io/ioutil"
+	"os"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/common/herrors"
@@ -108,7 +108,7 @@ func (c *Chain) Apply(to io.Writer, from io.Reader) error {
 		if err := tr(fb); err != nil {
 			// Write output to a temp file so it can be read by the user for trouble shooting.
 			filename := "output.html"
-			tempfile, ferr := ioutil.TempFile("", "hugo-transform-error")
+			tempfile, ferr := os.CreateTemp("", "hugo-transform-error")
 			if ferr == nil {
 				filename = tempfile.Name()
 				defer tempfile.Close()


Test patch:
diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go
index 47b5a7fcf42..6b96a8601e1 100644
--- a/cache/filecache/filecache_test.go
+++ b/cache/filecache/filecache_test.go
@@ -17,8 +17,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
-	"os"
 	"path/filepath"
 	"strings"
 	"sync"
@@ -44,13 +42,8 @@ func TestFileCache(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	tempWorkingDir, err := ioutil.TempDir("", "hugo_filecache_test_work")
-	c.Assert(err, qt.IsNil)
-	defer os.Remove(tempWorkingDir)
-
-	tempCacheDir, err := ioutil.TempDir("", "hugo_filecache_test_cache")
-	c.Assert(err, qt.IsNil)
-	defer os.Remove(tempCacheDir)
+	tempWorkingDir := t.TempDir()
+	tempCacheDir := t.TempDir()
 
 	osfs := afero.NewOsFs()
 
@@ -123,7 +116,7 @@ dir = ":cacheDir/c"
 					io.Closer
 				}{
 					strings.NewReader(s),
-					ioutil.NopCloser(nil),
+					io.NopCloser(nil),
 				}, nil
 			}
 		}
@@ -138,7 +131,7 @@ dir = ":cacheDir/c"
 				c.Assert(err, qt.IsNil)
 				c.Assert(r, qt.Not(qt.IsNil))
 				c.Assert(info.Name, qt.Equals, "a")
-				b, _ := ioutil.ReadAll(r)
+				b, _ := io.ReadAll(r)
 				r.Close()
 				c.Assert(string(b), qt.Equals, "abc")
 
@@ -154,7 +147,7 @@ dir = ":cacheDir/c"
 
 				_, r, err = ca.GetOrCreate("a", rf("bcd"))
 				c.Assert(err, qt.IsNil)
-				b, _ = ioutil.ReadAll(r)
+				b, _ = io.ReadAll(r)
 				r.Close()
 				c.Assert(string(b), qt.Equals, "abc")
 			}
@@ -173,7 +166,7 @@ dir = ":cacheDir/c"
 		c.Assert(err, qt.IsNil)
 		c.Assert(r, qt.Not(qt.IsNil))
 		c.Assert(info.Name, qt.Equals, "mykey")
-		b, _ := ioutil.ReadAll(r)
+		b, _ := io.ReadAll(r)
 		r.Close()
 		c.Assert(string(b), qt.Equals, "Hugo is great!")
 
@@ -233,7 +226,7 @@ dir = "/cache/c"
 					return hugio.ToReadCloser(strings.NewReader(data)), nil
 				})
 				c.Assert(err, qt.IsNil)
-				b, _ := ioutil.ReadAll(r)
+				b, _ := io.ReadAll(r)
 				r.Close()
 				c.Assert(string(b), qt.Equals, data)
 				// Trigger some expiration.
@@ -260,7 +253,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
 				return errors.New("fail")
 			}
 
-			b, _ := ioutil.ReadAll(r)
+			b, _ := io.ReadAll(r)
 			result = string(b)
 
 			return nil
diff --git a/commands/commands_test.go b/commands/commands_test.go
index 8f1ef825d94..03b59e19e98 100644
--- a/commands/commands_test.go
+++ b/commands/commands_test.go
@@ -15,7 +15,6 @@ package commands
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"testing"
@@ -402,7 +401,7 @@ PostProcess: {{ $foo.RelPermalink }}
 
 func writeFile(t testing.TB, filename, content string) {
 	must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
-	must(t, ioutil.WriteFile(filename, []byte(content), os.FileMode(0755)))
+	must(t, os.WriteFile(filename, []byte(content), os.FileMode(0755)))
 }
 
 func must(t testing.TB, err error) {
diff --git a/deploy/deploy_test.go b/deploy/deploy_test.go
index a92649b7420..5c436abf27e 100644
--- a/deploy/deploy_test.go
+++ b/deploy/deploy_test.go
@@ -23,7 +23,6 @@ import (
 	"crypto/md5"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"path/filepath"
@@ -407,7 +406,7 @@ func TestLocalFile(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			gotContent, err := ioutil.ReadAll(r)
+			gotContent, err := io.ReadAll(r)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -420,7 +419,7 @@ func TestLocalFile(t *testing.T) {
 			if err != nil {
 				t.Fatal(err)
 			}
-			gotContent, err = ioutil.ReadAll(r)
+			gotContent, err = io.ReadAll(r)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -520,47 +519,35 @@ type fsTest struct {
 // 1. An in-memory afero.Fs paired with an in-memory Go CDK bucket.
 // 2. A filesystem-based afero.Fs paired with an filesystem-based Go CDK bucket.
 // It returns the pair of tests and a cleanup function.
-func initFsTests() ([]*fsTest, func(), error) {
-	tmpfsdir, err := ioutil.TempDir("", "fs")
-	if err != nil {
-		return nil, nil, err
-	}
-	tmpbucketdir, err := ioutil.TempDir("", "bucket")
-	if err != nil {
-		return nil, nil, err
-	}
+func initFsTests(t *testing.T) []*fsTest {
+	t.Helper()
+
+	tmpfsdir := t.TempDir()
+	tmpbucketdir := t.TempDir()
 
 	memfs := afero.NewMemMapFs()
 	membucket := memblob.OpenBucket(nil)
+	t.Cleanup(func() { membucket.Close() })
 
 	filefs := afero.NewBasePathFs(afero.NewOsFs(), tmpfsdir)
 	filebucket, err := fileblob.OpenBucket(tmpbucketdir, nil)
 	if err != nil {
-		return nil, nil, err
+		t.Fatal(err)
 	}
+	t.Cleanup(func() { filebucket.Close() })
 
 	tests := []*fsTest{
 		{"mem", memfs, membucket},
 		{"file", filefs, filebucket},
 	}
-	cleanup := func() {
-		membucket.Close()
-		filebucket.Close()
-		os.RemoveAll(tmpfsdir)
-		os.RemoveAll(tmpbucketdir)
-	}
-	return tests, cleanup, nil
+	return tests
 }
 
 // TestEndToEndSync verifies that basic adds, updates, and deletes are working
 // correctly.
 func TestEndToEndSync(t *testing.T) {
 	ctx := context.Background()
-	tests, cleanup, err := initFsTests()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cleanup()
+	tests := initFsTests(t)
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			local, err := initLocalFs(ctx, test.fs)
@@ -643,11 +630,7 @@ func TestEndToEndSync(t *testing.T) {
 // TestMaxDeletes verifies that the "maxDeletes" flag is working correctly.
 func TestMaxDeletes(t *testing.T) {
 	ctx := context.Background()
-	tests, cleanup, err := initFsTests()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cleanup()
+	tests := initFsTests(t)
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			local, err := initLocalFs(ctx, test.fs)
@@ -772,14 +755,10 @@ func TestIncludeExclude(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(fmt.Sprintf("include %q exclude %q", test.Include, test.Exclude), func(t *testing.T) {
-			fsTests, cleanup, err := initFsTests()
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer cleanup()
+			fsTests := initFsTests(t)
 			fsTest := fsTests[1] // just do file-based test
 
-			_, err = initLocalFs(ctx, fsTest.fs)
+			_, err := initLocalFs(ctx, fsTest.fs)
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -841,11 +820,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(fmt.Sprintf("include %q exclude %q", test.Include, test.Exclude), func(t *testing.T) {
-			fsTests, cleanup, err := initFsTests()
-			if err != nil {
-				t.Fatal(err)
-			}
-			defer cleanup()
+			fsTests := initFsTests(t)
 			fsTest := fsTests[1] // just do file-based test
 
 			local, err := initLocalFs(ctx, fsTest.fs)
@@ -897,11 +872,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) {
 func TestCompression(t *testing.T) {
 	ctx := context.Background()
 
-	tests, cleanup, err := initFsTests()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cleanup()
+	tests := initFsTests(t)
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			local, err := initLocalFs(ctx, test.fs)
@@ -956,11 +927,7 @@ func TestCompression(t *testing.T) {
 // attribute for matcher works.
 func TestMatching(t *testing.T) {
 	ctx := context.Background()
-	tests, cleanup, err := initFsTests()
-	if err != nil {
-		t.Fatal(err)
-	}
-	defer cleanup()
+	tests := initFsTests(t)
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
 			_, err := initLocalFs(ctx, test.fs)
diff --git a/helpers/path_test.go b/helpers/path_test.go
index b8739f37ad5..1f206a88159 100644
--- a/helpers/path_test.go
+++ b/helpers/path_test.go
@@ -15,7 +15,6 @@ package helpers
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"reflect"
@@ -259,7 +258,7 @@ func TestIsDir(t *testing.T) {
 
 func createZeroSizedFileInTempDir() (*os.File, error) {
 	filePrefix := "_path_test_"
-	f, e := ioutil.TempFile("", filePrefix) // dir is os.TempDir()
+	f, e := os.CreateTemp("", filePrefix) // dir is os.TempDir()
 	if e != nil {
 		// if there was an error no file was created.
 		// => no requirement to delete the file
@@ -275,7 +274,7 @@ func createNonZeroSizedFileInTempDir() (*os.File, error) {
 		return nil, err
 	}
 	byteString := []byte("byteString")
-	err = ioutil.WriteFile(f.Name(), byteString, 0644)
+	err = os.WriteFile(f.Name(), byteString, 0644)
 	if err != nil {
 		// delete the file
 		deleteFileInTempDir(f)
@@ -288,27 +287,12 @@ func deleteFileInTempDir(f *os.File) {
 	_ = os.Remove(f.Name())
 }
 
-func createEmptyTempDir() (string, error) {
-	dirPrefix := "_dir_prefix_"
-	d, e := ioutil.TempDir("", dirPrefix) // will be in os.TempDir()
-	if e != nil {
-		// no directory to delete - it was never created
-		return "", e
-	}
-	return d, nil
-}
-
-func deleteTempDir(d string) {
-	_ = os.RemoveAll(d)
-}
-
 func TestExists(t *testing.T) {
 	zeroSizedFile, _ := createZeroSizedFileInTempDir()
 	defer deleteFileInTempDir(zeroSizedFile)
 	nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir()
 	defer deleteFileInTempDir(nonZeroSizedFile)
-	emptyDirectory, _ := createEmptyTempDir()
-	defer deleteTempDir(emptyDirectory)
+	emptyDirectory := t.TempDir()
 	nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt"
 	nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/"
 
@@ -455,8 +439,7 @@ func TestFindCWD(t *testing.T) {
 func TestSafeWriteToDisk(t *testing.T) {
 	emptyFile, _ := createZeroSizedFileInTempDir()
 	defer deleteFileInTempDir(emptyFile)
-	tmpDir, _ := createEmptyTempDir()
-	defer deleteTempDir(tmpDir)
+	tmpDir := t.TempDir()
 
 	randomString := "This is a random string!"
 	reader := strings.NewReader(randomString)
@@ -485,7 +468,7 @@ func TestSafeWriteToDisk(t *testing.T) {
 			if d.expectedErr != e {
 				t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedErr, e)
 			}
-			contents, _ := ioutil.ReadFile(d.filename)
+			contents, _ := os.ReadFile(d.filename)
 			if randomString != string(contents) {
 				t.Errorf("Test %d failed. Expected contents %q but got %q", i, randomString, string(contents))
 			}
@@ -497,8 +480,7 @@ func TestSafeWriteToDisk(t *testing.T) {
 func TestWriteToDisk(t *testing.T) {
 	emptyFile, _ := createZeroSizedFileInTempDir()
 	defer deleteFileInTempDir(emptyFile)
-	tmpDir, _ := createEmptyTempDir()
-	defer deleteTempDir(tmpDir)
+	tmpDir := t.TempDir()
 
 	randomString := "This is a random string!"
 	reader := strings.NewReader(randomString)
@@ -520,7 +502,7 @@ func TestWriteToDisk(t *testing.T) {
 		if d.expectedErr != e {
 			t.Errorf("Test %d failed. WriteToDisk Error Expected %q but got %q", i, d.expectedErr, e)
 		}
-		contents, e := ioutil.ReadFile(d.filename)
+		contents, e := os.ReadFile(d.filename)
 		if e != nil {
 			t.Errorf("Test %d failed. Could not read file %s. Reason: %s\n", i, d.filename, e)
 		}
diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go
index c843866fc04..dda4bed4ba1 100644
--- a/hugofs/rootmapping_fs_test.go
+++ b/hugofs/rootmapping_fs_test.go
@@ -15,7 +15,7 @@ package hugofs
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 	"path/filepath"
 	"sort"
 	"testing"
@@ -267,7 +267,7 @@ func TestRootMappingFsMount(t *testing.T) {
 	tf, err := testfilem.Open()
 	c.Assert(err, qt.IsNil)
 	defer tf.Close()
-	b, err := ioutil.ReadAll(tf)
+	b, err := io.ReadAll(tf)
 	c.Assert(err, qt.IsNil)
 	c.Assert(string(b), qt.Equals, "some no content")
 
diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go
index 6249c0aad40..823f7db45f8 100644
--- a/hugolib/resource_chain_test.go
+++ b/hugolib/resource_chain_test.go
@@ -16,7 +16,6 @@ package hugolib
 import (
 	"fmt"
 	"io"
-	"io/ioutil"
 	"math/rand"
 	"net/http"
 	"net/http/httptest"
@@ -305,7 +304,7 @@ func TestResourceChains(t *testing.T) {
 		case "/post":
 			w.Header().Set("Content-Type", "text/plain")
 			if r.Method == http.MethodPost {
-				body, err := ioutil.ReadAll(r.Body)
+				body, err := io.ReadAll(r.Body)
 				if err != nil {
 					http.Error(w, "Internal server error", http.StatusInternalServerError)
 					return
diff --git a/hugolib/site_stats_test.go b/hugolib/site_stats_test.go
index df1f64840da..4ed6411d212 100644
--- a/hugolib/site_stats_test.go
+++ b/hugolib/site_stats_test.go
@@ -16,7 +16,7 @@ package hugolib
 import (
 	"bytes"
 	"fmt"
-	"io/ioutil"
+	"io"
 	"testing"
 
 	"github.com/gohugoio/hugo/helpers"
@@ -87,8 +87,8 @@ aliases: [/Ali%d]
 		h.Sites[1].PathSpec.ProcessingStats,
 	}
 
-	stats[0].Table(ioutil.Discard)
-	stats[1].Table(ioutil.Discard)
+	stats[0].Table(io.Discard)
+	stats[1].Table(io.Discard)
 
 	var buff bytes.Buffer
 
diff --git a/hugolib/site_test.go b/hugolib/site_test.go
index a2ee5699478..494c41c88b3 100644
--- a/hugolib/site_test.go
+++ b/hugolib/site_test.go
@@ -17,7 +17,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"strings"
@@ -1094,7 +1093,7 @@ ABC.
 	b.Build(BuildCfg{})
 
 	contentMem := b.FileContent(statsFilename)
-	cb, err := ioutil.ReadFile(statsFilename)
+	cb, err := os.ReadFile(statsFilename)
 	b.Assert(err, qt.IsNil)
 	contentFile := string(cb)
 
diff --git a/media/mediaType_test.go b/media/mediaType_test.go
index 3d12c31bb69..4ddafc7c5f6 100644
--- a/media/mediaType_test.go
+++ b/media/mediaType_test.go
@@ -15,7 +15,7 @@ package media
 
 import (
 	"encoding/json"
-	"io/ioutil"
+	"os"
 	"path/filepath"
 	"sort"
 	"strings"
@@ -190,7 +190,7 @@ func TestFromContent(t *testing.T) {
 	for _, filename := range files {
 		name := filepath.Base(filename)
 		c.Run(name, func(c *qt.C) {
-			content, err := ioutil.ReadFile(filename)
+			content, err := os.ReadFile(filename)
 			c.Assert(err, qt.IsNil)
 			ext := strings.TrimPrefix(paths.Ext(filename), ".")
 			var exts []string
@@ -217,7 +217,7 @@ func TestFromContentFakes(t *testing.T) {
 	for _, filename := range files {
 		name := filepath.Base(filename)
 		c.Run(name, func(c *qt.C) {
-			content, err := ioutil.ReadFile(filename)
+			content, err := os.ReadFile(filename)
 			c.Assert(err, qt.IsNil)
 			ext := strings.TrimPrefix(paths.Ext(filename), ".")
 			got := FromContent(mtypes, []string{ext}, content)
diff --git a/resources/image_test.go b/resources/image_test.go
index 3cb1089f4a1..e5d0fd620ca 100644
--- a/resources/image_test.go
+++ b/resources/image_test.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 	"image"
 	"image/gif"
-	"io/ioutil"
+	"io/fs"
 	"math/big"
 	"math/rand"
 	"os"
@@ -57,6 +57,14 @@ var eq = qt.CmpEquals(
 	cmp.Comparer(func(p1, p2 os.FileInfo) bool {
 		return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
 	}),
+	cmp.Comparer(func(d1, d2 fs.DirEntry) bool {
+		p1, err1 := d1.Info()
+		p2, err2 := d2.Info()
+		if err1 != nil || err2 != nil {
+			return false
+		}
+		return p1.Name() == p2.Name() && p1.Size() == p2.Size() && p1.IsDir() == p2.IsDir()
+	}),
 	cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }),
 	cmp.Comparer(func(m1, m2 media.Type) bool {
 		return m1.Type() == m2.Type()
@@ -750,9 +758,9 @@ func TestImageOperationsGolden(t *testing.T) {
 func assetGoldenDirs(c *qt.C, dir1, dir2 string) {
 
 	// The two dirs above should now be the same.
-	dirinfos1, err := ioutil.ReadDir(dir1)
+	dirinfos1, err := os.ReadDir(dir1)
 	c.Assert(err, qt.IsNil)
-	dirinfos2, err := ioutil.ReadDir(dir2)
+	dirinfos2, err := os.ReadDir(dir2)
 	c.Assert(err, qt.IsNil)
 	c.Assert(len(dirinfos1), qt.Equals, len(dirinfos2))
 
diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go
index 3a4e7e5805e..09268402e53 100644
--- a/resources/testhelpers_test.go
+++ b/resources/testhelpers_test.go
@@ -3,7 +3,6 @@ package resources
 import (
 	"image"
 	"io"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -106,7 +105,7 @@ func newTestResourceOsFs(c *qt.C) (*Spec, string) {
 	cfg := createTestCfg()
 	cfg.Set("baseURL", "https://example.com")
 
-	workDir, err := ioutil.TempDir("", "hugores")
+	workDir, err := os.MkdirTemp("", "hugores")
 	c.Assert(err, qt.IsNil)
 	c.Assert(workDir, qt.Not(qt.Equals), "")
 
diff --git a/watcher/filenotify/poller_test.go b/watcher/filenotify/poller_test.go
index b4723c7585d..7416f793a47 100644
--- a/watcher/filenotify/poller_test.go
+++ b/watcher/filenotify/poller_test.go
@@ -4,7 +4,6 @@ package filenotify
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -35,11 +34,14 @@ func TestPollerAddRemove(t *testing.T) {
 	c.Assert(w.Add("foo"), qt.Not(qt.IsNil))
 	c.Assert(w.Remove("foo"), qt.Not(qt.IsNil))
 
-	f, err := ioutil.TempFile("", "asdf")
+	f, err := os.CreateTemp("", "asdf")
 	if err != nil {
 		t.Fatal(err)
 	}
-	defer os.RemoveAll(f.Name())
+	c.Cleanup(func() {
+		c.Assert(w.Close(), qt.IsNil)
+		os.Remove(f.Name())
+	})
 	c.Assert(w.Add(f.Name()), qt.IsNil)
 	c.Assert(w.Remove(f.Name()), qt.IsNil)
 
@@ -66,7 +68,7 @@ func TestPollerEvent(t *testing.T) {
 			filename := filepath.Join(subdir, "file1")
 
 			// Write to one file.
-			c.Assert(ioutil.WriteFile(filename, []byte("changed"), 0600), qt.IsNil)
+			c.Assert(os.WriteFile(filename, []byte("changed"), 0600), qt.IsNil)
 
 			var expected []fsnotify.Event
 
@@ -86,7 +88,7 @@ func TestPollerEvent(t *testing.T) {
 
 			// Add one file.
 			filename = filepath.Join(subdir, "file3")
-			c.Assert(ioutil.WriteFile(filename, []byte("new"), 0600), qt.IsNil)
+			c.Assert(os.WriteFile(filename, []byte("new"), 0600), qt.IsNil)
 			assertEvents(c, w, fsnotify.Event{Name: filename, Op: fsnotify.Create})
 
 			// Remove entire directory.
@@ -127,9 +129,10 @@ func TestPollerEvent(t *testing.T) {
 func TestPollerClose(t *testing.T) {
 	c := qt.New(t)
 	w := NewPollingWatcher(watchWaitTime)
-	f1, err := ioutil.TempFile("", "f1")
+	f1, err := os.CreateTemp("", "f1")
 	c.Assert(err, qt.IsNil)
-	f2, err := ioutil.TempFile("", "f2")
+	defer os.Remove(f1.Name())
+	f2, err := os.CreateTemp("", "f2")
 	c.Assert(err, qt.IsNil)
 	filename1 := f1.Name()
 	filename2 := f2.Name()
@@ -140,12 +143,12 @@ func TestPollerClose(t *testing.T) {
 	c.Assert(w.Add(filename2), qt.IsNil)
 	c.Assert(w.Close(), qt.IsNil)
 	c.Assert(w.Close(), qt.IsNil)
-	c.Assert(ioutil.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
-	c.Assert(ioutil.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
+	c.Assert(os.WriteFile(filename1, []byte("new"), 0600), qt.IsNil)
+	c.Assert(os.WriteFile(filename2, []byte("new"), 0600), qt.IsNil)
 	// No more event as the watchers are closed.
 	assertEvents(c, w)
 
-	f2, err = ioutil.TempFile("", "f2")
+	f2, err = os.CreateTemp("", "f2")
 	c.Assert(err, qt.IsNil)
 
 	defer os.Remove(f2.Name())
@@ -172,7 +175,7 @@ func TestCheckChange(t *testing.T) {
 	c.Assert(os.Chmod(filepath.Join(filepath.Join(dir, subdir2, "file1")), 0400), qt.IsNil)
 	f1_2 := stat(subdir2, "file1")
 
-	c.Assert(ioutil.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
+	c.Assert(os.WriteFile(filepath.Join(filepath.Join(dir, subdir2, "file2")), []byte("changed"), 0600), qt.IsNil)
 	f2_2 := stat(subdir2, "file2")
 
 	c.Assert(checkChange(f0, nil), qt.Equals, fsnotify.Remove)
@@ -221,17 +224,16 @@ func BenchmarkPoller(b *testing.B) {
 }
 
 func prepareTestDirWithSomeFiles(c *qt.C, id string) string {
-	dir, err := ioutil.TempDir("", fmt.Sprintf("test-poller-dir-%s", id))
-	c.Assert(err, qt.IsNil)
+	dir := c.TB.TempDir()
 	c.Assert(os.MkdirAll(filepath.Join(dir, subdir1), 0777), qt.IsNil)
 	c.Assert(os.MkdirAll(filepath.Join(dir, subdir2), 0777), qt.IsNil)
 
 	for i := 0; i < 3; i++ {
-		c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil)
+		c.Assert(os.WriteFile(filepath.Join(dir, subdir1, fmt.Sprintf("file%d", i)), []byte("hello1"), 0600), qt.IsNil)
 	}
 
 	for i := 0; i < 3; i++ {
-		c.Assert(ioutil.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil)
+		c.Assert(os.WriteFile(filepath.Join(dir, subdir2, fmt.Sprintf("file%d", i)), []byte("hello2"), 0600), qt.IsNil)
 	}
 
 	c.Cleanup(func() {