Commit 29761a64 authored by Adrian Sutton's avatar Adrian Sutton Committed by GitHub

op-challenger: Support multiple prestates (#10282)

* op-challenger: Implement prestate sources

* op-challenger: Add canon-prestates-url flag to configure a source for prestates based on hash
parent 66a21a9a
......@@ -462,7 +462,7 @@ func TestCannonRequiredArgs(t *testing.T) {
})
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag cannon-prestate is required", addRequiredArgsExcept(traceType, "--cannon-prestate"))
verifyArgsInvalid(t, "flag cannon-prestates-url or cannon-prestate is required", addRequiredArgsExcept(traceType, "--cannon-prestate"))
})
t.Run("Valid", func(t *testing.T) {
......@@ -471,6 +471,21 @@ func TestCannonRequiredArgs(t *testing.T) {
})
})
t.Run(fmt.Sprintf("TestCannonAbsolutePrestateBaseURL-%v", traceType), func(t *testing.T) {
t.Run("NotRequiredForAlphabetTrace", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-prestates-url"))
})
t.Run("Required", func(t *testing.T) {
verifyArgsInvalid(t, "flag cannon-prestates-url or cannon-prestate is required", addRequiredArgsExcept(traceType, "--cannon-prestate"))
})
t.Run("Valid", func(t *testing.T) {
cfg := configForArgs(t, addRequiredArgsExcept(traceType, "--cannon-prestates-url", "--cannon-prestates-url=http://localhost/foo"))
require.Equal(t, "http://localhost/foo", cfg.CannonAbsolutePreStateBaseURL.String())
})
})
t.Run(fmt.Sprintf("TestL2Rpc-%v", traceType), func(t *testing.T) {
t.Run("NotRequiredForAlphabetTraceLegacy", func(t *testing.T) {
configForArgs(t, addRequiredArgsExcept(config.TraceTypeAlphabet, "--cannon-l2"))
......
......@@ -3,6 +3,7 @@ package config
import (
"errors"
"fmt"
"net/url"
"runtime"
"slices"
"time"
......@@ -23,6 +24,7 @@ var (
ErrMissingCannonBin = errors.New("missing cannon bin")
ErrMissingCannonServer = errors.New("missing cannon server")
ErrMissingCannonAbsolutePreState = errors.New("missing cannon absolute pre-state")
ErrCannonAbsolutePreStateAndBaseURL = errors.New("only specify one of cannon absolute pre-state and cannon absolute pre-state base URL")
ErrMissingL1EthRPC = errors.New("missing l1 eth rpc url")
ErrMissingL1Beacon = errors.New("missing l1 beacon url")
ErrMissingGameFactoryAddress = errors.New("missing game factory address")
......@@ -127,6 +129,7 @@ type Config struct {
CannonBin string // Path to the cannon executable to run when generating trace data
CannonServer string // Path to the op-program executable that provides the pre-image oracle server
CannonAbsolutePreState string // File to load the absolute pre-state for Cannon traces from
CannonAbsolutePreStateBaseURL *url.URL // Base URL to retrieve absolute pre-states for Cannon traces from
CannonNetwork string
CannonRollupConfigPath string
CannonL2GenesisPath string
......@@ -233,9 +236,12 @@ func (c Config) Check() error {
return fmt.Errorf("%w: %v", ErrCannonNetworkUnknown, c.CannonNetwork)
}
}
if c.CannonAbsolutePreState == "" {
if c.CannonAbsolutePreState == "" && c.CannonAbsolutePreStateBaseURL == nil {
return ErrMissingCannonAbsolutePreState
}
if c.CannonAbsolutePreState != "" && c.CannonAbsolutePreStateBaseURL != nil {
return ErrCannonAbsolutePreStateAndBaseURL
}
if c.L2Rpc == "" {
return ErrMissingL2Rpc
}
......
......@@ -2,6 +2,7 @@ package config
import (
"fmt"
"net/url"
"runtime"
"testing"
......@@ -19,6 +20,7 @@ var (
validCannonOpProgramBin = "./bin/op-program"
validCannonNetwork = "mainnet"
validCannonAbsolutPreState = "pre.json"
validCannonAbsolutPreStateBaseURL, _ = url.Parse("http://localhost/foo/")
validDatadir = "/tmp/data"
validL2Rpc = "http://localhost:9545"
validRollupRpc = "http://localhost:8555"
......@@ -34,7 +36,7 @@ var cannonTraceTypes = []TraceType{TraceTypeCannon, TraceTypePermissioned}
func applyValidConfigForCannon(cfg *Config) {
cfg.CannonBin = validCannonBin
cfg.CannonServer = validCannonOpProgramBin
cfg.CannonAbsolutePreState = validCannonAbsolutPreState
cfg.CannonAbsolutePreStateBaseURL = validCannonAbsolutPreStateBaseURL
cfg.CannonNetwork = validCannonNetwork
cfg.L2Rpc = validL2Rpc
}
......@@ -124,12 +126,34 @@ func TestCannonRequiredArgs(t *testing.T) {
require.ErrorIs(t, config.Check(), ErrMissingCannonServer)
})
t.Run(fmt.Sprintf("TestCannonAbsolutePreStateRequired-%v", traceType), func(t *testing.T) {
t.Run(fmt.Sprintf("TestCannonAbsolutePreStateOrBaseURLRequired-%v", traceType), func(t *testing.T) {
config := validConfig(traceType)
config.CannonAbsolutePreState = ""
config.CannonAbsolutePreStateBaseURL = nil
require.ErrorIs(t, config.Check(), ErrMissingCannonAbsolutePreState)
})
t.Run(fmt.Sprintf("TestCannonAbsolutePreState-%v", traceType), func(t *testing.T) {
config := validConfig(traceType)
config.CannonAbsolutePreState = validCannonAbsolutPreState
config.CannonAbsolutePreStateBaseURL = nil
require.NoError(t, config.Check())
})
t.Run(fmt.Sprintf("TestCannonAbsolutePreStateBaseURL-%v", traceType), func(t *testing.T) {
config := validConfig(traceType)
config.CannonAbsolutePreState = ""
config.CannonAbsolutePreStateBaseURL = validCannonAbsolutPreStateBaseURL
require.NoError(t, config.Check())
})
t.Run(fmt.Sprintf("TestMustNotSupplyBothCannonAbsolutePreStateAndBaseURL-%v", traceType), func(t *testing.T) {
config := validConfig(traceType)
config.CannonAbsolutePreState = validCannonAbsolutPreState
config.CannonAbsolutePreStateBaseURL = validCannonAbsolutPreStateBaseURL
require.ErrorIs(t, config.Check(), ErrCannonAbsolutePreStateAndBaseURL)
})
t.Run(fmt.Sprintf("TestL2RpcRequired-%v", traceType), func(t *testing.T) {
config := validConfig(traceType)
config.L2Rpc = ""
......@@ -238,6 +262,7 @@ func TestRequireConfigForMultipleTraceTypesForCannon(t *testing.T) {
// Require cannon specific args
cfg.CannonAbsolutePreState = ""
cfg.CannonAbsolutePreStateBaseURL = nil
require.ErrorIs(t, cfg.Check(), ErrMissingCannonAbsolutePreState)
cfg.CannonAbsolutePreState = validCannonAbsolutPreState
......
......@@ -2,6 +2,7 @@ package flags
import (
"fmt"
"net/url"
"runtime"
"slices"
"strings"
......@@ -126,6 +127,12 @@ var (
Usage: "Path to absolute prestate to use when generating trace data (cannon trace type only)",
EnvVars: prefixEnvVars("CANNON_PRESTATE"),
}
CannonPreStatesURLFlag = &cli.StringFlag{
Name: "cannon-prestates-url",
Usage: "Base URL to absolute prestates to use when generating trace data. " +
"Prestates in this directory should be name as <commitment>.json (cannon trace type only)",
EnvVars: prefixEnvVars("CANNON_PRESTATES_URL"),
}
CannonL2Flag = &cli.StringFlag{
Name: "cannon-l2",
Usage: fmt.Sprintf("Deprecated: Use %v instead", L2RpcFlag.Name),
......@@ -232,6 +239,7 @@ var optionalFlags = []cli.Flag{
CannonBinFlag,
CannonServerFlag,
CannonPreStateFlag,
CannonPreStatesURLFlag,
CannonL2Flag,
CannonSnapshotFreqFlag,
CannonInfoFreqFlag,
......@@ -277,8 +285,8 @@ func CheckCannonFlags(ctx *cli.Context) error {
if !ctx.IsSet(CannonServerFlag.Name) {
return fmt.Errorf("flag %s is required", CannonServerFlag.Name)
}
if !ctx.IsSet(CannonPreStateFlag.Name) {
return fmt.Errorf("flag %s is required", CannonPreStateFlag.Name)
if !ctx.IsSet(CannonPreStateFlag.Name) && !ctx.IsSet(CannonPreStatesURLFlag.Name) {
return fmt.Errorf("flag %s or %s is required", CannonPreStatesURLFlag.Name, CannonPreStateFlag.Name)
}
// CannonL2Flag is checked because it is an alias with L2RpcFlag
if !ctx.IsSet(CannonL2Flag.Name) && !ctx.IsSet(L2RpcFlag.Name) {
......@@ -408,6 +416,14 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
claimants = append(claimants, claimant)
}
}
var cannonPrestatesURL *url.URL
if ctx.IsSet(CannonPreStatesURLFlag.Name) {
parsed, err := url.Parse(ctx.String(CannonPreStatesURLFlag.Name))
if err != nil {
return nil, fmt.Errorf("invalid cannon pre states url (%v): %w", ctx.String(CannonPreStatesURLFlag.Name), err)
}
cannonPrestatesURL = parsed
}
l2Rpc, err := getL2Rpc(ctx)
if err != nil {
return nil, err
......@@ -432,6 +448,7 @@ func NewConfigFromCLI(ctx *cli.Context) (*config.Config, error) {
CannonBin: ctx.String(CannonBinFlag.Name),
CannonServer: ctx.String(CannonServerFlag.Name),
CannonAbsolutePreState: ctx.String(CannonPreStateFlag.Name),
CannonAbsolutePreStateBaseURL: cannonPrestatesURL,
Datadir: ctx.String(DatadirFlag.Name),
CannonSnapshotFreq: ctx.Uint(CannonSnapshotFreqFlag.Name),
CannonInfoFreq: ctx.Uint(CannonInfoFreqFlag.Name),
......
......@@ -3,6 +3,7 @@ package fault
import (
"context"
"fmt"
"path/filepath"
"github.com/ethereum-optimism/optimism/op-challenger/config"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/claims"
......@@ -11,6 +12,7 @@ import (
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/asterisc"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/cannon"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/outputs"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/prestates"
"github.com/ethereum-optimism/optimism/op-challenger/game/fault/trace/utils"
faultTypes "github.com/ethereum-optimism/optimism/op-challenger/game/fault/types"
keccakTypes "github.com/ethereum-optimism/optimism/op-challenger/game/keccak/types"
......@@ -36,6 +38,13 @@ type OracleRegistry interface {
RegisterOracle(oracle keccakTypes.LargePreimageOracle)
}
type PrestateSource interface {
// PrestatePath returns the path to the prestate file to use for the game.
// The provided prestateHash may be used to differentiate between different states but no guarantee is made that
// the returned prestate matches the supplied hash.
PrestatePath(prestateHash common.Hash) (string, error)
}
type RollupClient interface {
outputs.OutputRollupClient
SyncStatusProvider
......@@ -253,9 +262,25 @@ func registerCannon(
selective bool,
claimants []common.Address,
) error {
cannonPrestateProvider := cannon.NewPrestateProvider(cfg.CannonAbsolutePreState)
var prestateSource PrestateSource
if cfg.CannonAbsolutePreStateBaseURL != nil {
prestateSource = prestates.NewMultiPrestateProvider(cfg.CannonAbsolutePreStateBaseURL, filepath.Join(cfg.Datadir, "cannon-prestates"))
} else {
prestateSource = prestates.NewSinglePrestateSource(cfg.CannonAbsolutePreState)
}
playerCreator := func(game types.GameMetadata, dir string) (scheduler.GamePlayer, error) {
contract := contracts.NewFaultDisputeGameContract(m, game.Proxy, caller)
requiredPrestatehash, err := contract.GetAbsolutePrestateHash(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load prestate hash for game %v: %w", game.Proxy, err)
}
prestatePath, err := prestateSource.PrestatePath(requiredPrestatehash)
if err != nil {
return nil, fmt.Errorf("required prestate %v not available for game %v: %w", requiredPrestatehash, game.Proxy, err)
}
cannonPrestateProvider := cannon.NewPrestateProvider(prestatePath)
oracle, err := contract.GetOracle(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load oracle for game %v: %w", game.Proxy, err)
......
......@@ -34,12 +34,14 @@ type CannonTraceProvider struct {
gameDepth types.Depth
preimageLoader *utils.PreimageLoader
types.PrestateProvider
// lastStep stores the last step in the actual trace if known. 0 indicates unknown.
// Cached as an optimisation to avoid repeatedly attempting to execute beyond the end of the trace.
lastStep uint64
}
func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, localInputs utils.LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider {
func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, prestateProvider types.PrestateProvider, localInputs utils.LocalGameInputs, dir string, gameDepth types.Depth) *CannonTraceProvider {
return &CannonTraceProvider{
logger: logger,
dir: dir,
......@@ -47,6 +49,7 @@ func NewTraceProvider(logger log.Logger, m CannonMetricer, cfg *config.Config, l
generator: NewExecutor(logger, m, cfg, localInputs),
gameDepth: gameDepth,
preimageLoader: utils.NewPreimageLoader(kvstore.NewDiskKV(utils.PreimageDir(dir)).Get),
PrestateProvider: prestateProvider,
}
}
......@@ -91,26 +94,6 @@ func (p *CannonTraceProvider) GetStepData(ctx context.Context, pos types.Positio
return value, data, oracleData, nil
}
func (p *CannonTraceProvider) absolutePreState() ([]byte, error) {
state, err := parseState(p.prestate)
if err != nil {
return nil, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
return state.EncodeWitness(), nil
}
func (p *CannonTraceProvider) AbsolutePreStateCommitment(_ context.Context) (common.Hash, error) {
state, err := p.absolutePreState()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot load absolute pre-state: %w", err)
}
hash, err := mipsevm.StateWitness(state).StateHash()
if err != nil {
return common.Hash{}, fmt.Errorf("cannot hash absolute pre-state: %w", err)
}
return hash, nil
}
// loadProof will attempt to load or generate the proof data at the specified index
// If the requested index is beyond the end of the actual trace it is extended with no-op instructions.
func (p *CannonTraceProvider) loadProof(ctx context.Context, i uint64) (*utils.ProofData, error) {
......
......@@ -3,6 +3,7 @@ package cannon
import (
"encoding/json"
"fmt"
"io"
"github.com/ethereum-optimism/optimism/cannon/mipsevm"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
......@@ -13,11 +14,14 @@ func parseState(path string) (*mipsevm.State, error) {
if err != nil {
return nil, fmt.Errorf("cannot open state file (%v): %w", path, err)
}
defer file.Close()
return parseStateFromReader(file)
}
func parseStateFromReader(in io.ReadCloser) (*mipsevm.State, error) {
defer in.Close()
var state mipsevm.State
err = json.NewDecoder(file).Decode(&state)
if err != nil {
return nil, fmt.Errorf("invalid mipsevm state (%v): %w", path, err)
if err := json.NewDecoder(in).Decode(&state); err != nil {
return nil, fmt.Errorf("invalid mipsevm state: %w", err)
}
return &state, nil
}
......@@ -39,7 +39,7 @@ func NewOutputCannonTraceAccessor(
if err != nil {
return nil, fmt.Errorf("failed to fetch cannon local inputs: %w", err)
}
provider := cannon.NewTraceProvider(logger, m, cfg, localInputs, subdir, depth)
provider := cannon.NewTraceProvider(logger, m, cfg, prestateProvider, localInputs, subdir, depth)
return provider, nil
}
......
package prestates
import (
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum/go-ethereum/common"
)
var (
ErrPrestateUnavailable = errors.New("prestate unavailable")
)
type MultiPrestateProvider struct {
baseUrl *url.URL
dataDir string
}
func NewMultiPrestateProvider(baseUrl *url.URL, dataDir string) *MultiPrestateProvider {
return &MultiPrestateProvider{
baseUrl: baseUrl,
dataDir: dataDir,
}
}
func (m *MultiPrestateProvider) PrestatePath(hash common.Hash) (string, error) {
path := filepath.Join(m.dataDir, hash.Hex()+".json.gz")
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
if err := m.fetchPrestate(hash, path); err != nil {
return "", fmt.Errorf("failed to fetch prestate: %w", err)
}
} else if err != nil {
return "", fmt.Errorf("error checking for existing prestate %v: %w", hash, err)
}
return path, nil
}
func (m *MultiPrestateProvider) fetchPrestate(hash common.Hash, dest string) error {
if err := os.MkdirAll(m.dataDir, 0755); err != nil {
return fmt.Errorf("error creating prestate dir: %w", err)
}
prestateUrl := m.baseUrl.JoinPath(hash.Hex() + ".json")
resp, err := http.Get(prestateUrl.String())
if err != nil {
return fmt.Errorf("failed to fetch prestate from %v: %w", prestateUrl, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("%w from url %v: status %v", ErrPrestateUnavailable, prestateUrl, resp.StatusCode)
}
out, err := ioutil.NewAtomicWriterCompressed(dest, 0o644)
if err != nil {
return fmt.Errorf("failed to open atomic writer for %v: %w", dest, err)
}
defer func() {
// If errors occur, try to clean up without renaming the file into its final destination as Close() would do
_ = out.Abort()
}()
if _, err := io.Copy(out, resp.Body); err != nil {
return fmt.Errorf("failed to write file %v: %w", dest, err)
}
if err := out.Close(); err != nil {
return fmt.Errorf("failed to close file %v: %w", dest, err)
}
return nil
}
package prestates
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/op-service/ioutil"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/require"
)
func TestDownloadPrestate(t *testing.T) {
dir := t.TempDir()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.URL.Path))
}))
defer server.Close()
provider := NewMultiPrestateProvider(parseURL(t, server.URL), dir)
hash := common.Hash{0xaa}
path, err := provider.PrestatePath(hash)
require.NoError(t, err)
in, err := ioutil.OpenDecompressed(path)
require.NoError(t, err)
defer in.Close()
content, err := io.ReadAll(in)
require.NoError(t, err)
require.Equal(t, "/"+hash.Hex()+".json", string(content))
}
func TestCreateDirectory(t *testing.T) {
dir := t.TempDir()
dir = filepath.Join(dir, "test")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(r.URL.Path))
}))
defer server.Close()
provider := NewMultiPrestateProvider(parseURL(t, server.URL), dir)
hash := common.Hash{0xaa}
path, err := provider.PrestatePath(hash)
require.NoError(t, err)
in, err := ioutil.OpenDecompressed(path)
require.NoError(t, err)
defer in.Close()
content, err := io.ReadAll(in)
require.NoError(t, err)
require.Equal(t, "/"+hash.Hex()+".json", string(content))
}
func TestExistingPrestate(t *testing.T) {
dir := t.TempDir()
provider := NewMultiPrestateProvider(parseURL(t, "http://127.0.0.1:1"), dir)
hash := common.Hash{0xaa}
expectedFile := filepath.Join(dir, hash.Hex()+".json.gz")
err := ioutil.WriteCompressedBytes(expectedFile, []byte("expected content"), os.O_WRONLY|os.O_CREATE, 0o644)
require.NoError(t, err)
path, err := provider.PrestatePath(hash)
require.NoError(t, err)
require.Equal(t, expectedFile, path)
in, err := ioutil.OpenDecompressed(path)
require.NoError(t, err)
defer in.Close()
content, err := io.ReadAll(in)
require.NoError(t, err)
require.Equal(t, "expected content", string(content))
}
func TestMissingPrestate(t *testing.T) {
dir := t.TempDir()
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(404)
}))
defer server.Close()
provider := NewMultiPrestateProvider(parseURL(t, server.URL), dir)
hash := common.Hash{0xaa}
path, err := provider.PrestatePath(hash)
require.ErrorIs(t, err, ErrPrestateUnavailable)
_, err = os.Stat(path)
require.ErrorIs(t, err, os.ErrNotExist)
}
func parseURL(t *testing.T, str string) *url.URL {
parsed, err := url.Parse(str)
require.NoError(t, err)
return parsed
}
package prestates
import "github.com/ethereum/go-ethereum/common"
type SinglePrestateSource struct {
path string
}
func NewSinglePrestateSource(path string) *SinglePrestateSource {
return &SinglePrestateSource{path: path}
}
func (s *SinglePrestateSource) PrestatePath(_ common.Hash) (string, error) {
return s.path, nil
}
......@@ -6,7 +6,7 @@ import (
"path/filepath"
)
type atomicWriter struct {
type AtomicWriter struct {
dest string
temp string
out io.WriteCloser
......@@ -16,7 +16,7 @@ type atomicWriter struct {
// The contents are initially written to a temporary file and only renamed into place when the writer is closed.
// NOTE: It's vital to check if an error is returned from Close() as it may indicate the file could not be renamed
// If path ends in .gz the contents written will be gzipped.
func NewAtomicWriterCompressed(path string, perm os.FileMode) (io.WriteCloser, error) {
func NewAtomicWriterCompressed(path string, perm os.FileMode) (*AtomicWriter, error) {
f, err := os.CreateTemp(filepath.Dir(path), filepath.Base(path))
if err != nil {
return nil, err
......@@ -25,18 +25,26 @@ func NewAtomicWriterCompressed(path string, perm os.FileMode) (io.WriteCloser, e
_ = f.Close()
return nil, err
}
return &atomicWriter{
return &AtomicWriter{
dest: path,
temp: f.Name(),
out: CompressByFileType(path, f),
}, nil
}
func (a *atomicWriter) Write(p []byte) (n int, err error) {
func (a *AtomicWriter) Write(p []byte) (n int, err error) {
return a.out.Write(p)
}
func (a *atomicWriter) Close() error {
// Abort releases any open resources and cleans up temporary files without renaming them into place.
// Does nothing if the writer has already been closed.
func (a *AtomicWriter) Abort() error {
// Attempt to clean up the temp file even if Close fails.
defer os.Remove(a.temp)
return a.out.Close()
}
func (a *AtomicWriter) Close() error {
// Attempt to clean up the temp file even if it can't be renamed into place.
defer os.Remove(a.temp)
if err := a.out.Close(); err != nil {
......
......@@ -46,6 +46,30 @@ func TestAtomicWriter_MultipleClose(t *testing.T) {
require.ErrorIs(t, f.Close(), os.ErrClosed)
}
func TestAtomicWriter_AbortBeforeClose(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
f, err := NewAtomicWriterCompressed(target, 0755)
require.NoError(t, err)
require.NoError(t, f.Abort())
_, err = os.Stat(target)
require.ErrorIs(t, err, os.ErrNotExist, "should not create target file when aborted")
require.ErrorIs(t, f.Close(), os.ErrClosed)
}
func TestAtomicWriter_AbortAfterClose(t *testing.T) {
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
f, err := NewAtomicWriterCompressed(target, 0755)
require.NoError(t, err)
require.NoError(t, f.Close())
_, err = os.Stat(target)
require.NoError(t, err)
require.ErrorIs(t, f.Abort(), os.ErrClosed)
}
func TestAtomicWriter_ApplyGzip(t *testing.T) {
tests := []struct {
name string
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment