add env parser

This commit is contained in:
2025-09-04 22:43:52 +04:00
parent fd7c4c6454
commit 81383d1ea7
10 changed files with 1466 additions and 30 deletions

View File

@@ -4,16 +4,27 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/aykhans/dodo/pkg/config" "github.com/aykhans/dodo/pkg/config/parser"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils" "github.com/aykhans/dodo/pkg/utils"
"github.com/jedib0t/go-pretty/v6/text" "github.com/jedib0t/go-pretty/v6/text"
"github.com/k0kubun/pp/v3"
) )
func main() { func main() {
cliParser := config.NewConfigCLIParser(os.Args) envParser := parser.NewConfigENVParser("DODO")
cfg, err := cliParser.Parse() envConfig, err := envParser.Parse()
_ = utils.HandleErrorOrDie(err,
utils.OnCustomError(func(err types.FieldParseErrors) error {
printValidationErrors("ENV", err.Errors...)
fmt.Println()
os.Exit(1)
return nil
}),
)
cliParser := parser.NewConfigCLIParser(os.Args)
cliConf, err := cliParser.Parse()
_ = utils.HandleErrorOrDie(err, _ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error { utils.OnSentinelError(types.ErrCLINoArgs, func(err error) error {
cliParser.PrintHelp() cliParser.PrintHelp()
@@ -35,7 +46,9 @@ func main() {
}), }),
) )
fmt.Println(cfg) envConfig.Merge(cliConf)
pp.Println(cliConf) //nolint
pp.Println(envConfig) //nolint
} }
func printValidationErrors(parserName string, errors ...types.FieldParseError) { func printValidationErrors(parserName string, errors ...types.FieldParseError) {

5
go.mod
View File

@@ -4,15 +4,18 @@ go 1.25
require ( require (
github.com/jedib0t/go-pretty/v6 v6.6.8 github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/k0kubun/pp/v3 v3.5.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

11
go.sum
View File

@@ -2,6 +2,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc= github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/k0kubun/pp/v3 v3.5.0 h1:iYNlYA5HJAJvkD4ibuf9c8y6SHM0QFhaBuCqm1zHp0w=
github.com/k0kubun/pp/v3 v3.5.0/go.mod h1:5lzno5ZZeEeTV/Ky6vs3g6d1U3WarDrH8k240vMtGro=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -11,10 +17,11 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -49,7 +49,7 @@ func NewConfig() *Config {
return &Config{} return &Config{}
} }
func (config *Config) MergeConfig(newConfig *Config) { func (config *Config) Merge(newConfig *Config) {
config.Files = append(config.Files, newConfig.Files...) config.Files = append(config.Files, newConfig.Files...)
if newConfig.Method != nil { if newConfig.Method != nil {
config.Method = newConfig.Method config.Method = newConfig.Method
@@ -75,19 +75,19 @@ func (config *Config) MergeConfig(newConfig *Config) {
if newConfig.SkipVerify != nil { if newConfig.SkipVerify != nil {
config.SkipVerify = newConfig.SkipVerify config.SkipVerify = newConfig.SkipVerify
} }
if len(newConfig.Params) != 0 { if len(newConfig.Params) != 0 { // TODO: append
config.Params = newConfig.Params config.Params = newConfig.Params
} }
if len(newConfig.Headers) != 0 { if len(newConfig.Headers) != 0 { // TODO: append
config.Headers = newConfig.Headers config.Headers = newConfig.Headers
} }
if len(newConfig.Cookies) != 0 { if len(newConfig.Cookies) != 0 { // TODO: append
config.Cookies = newConfig.Cookies config.Cookies = newConfig.Cookies
} }
if len(newConfig.Bodies) != 0 { if len(newConfig.Bodies) != 0 { // TODO: append
config.Bodies = newConfig.Bodies config.Bodies = newConfig.Bodies
} }
if len(newConfig.Proxies) != 0 { if len(newConfig.Proxies) != 0 { // TODO: append
config.Proxies = newConfig.Proxies config.Proxies = newConfig.Proxies
} }
} }

View File

@@ -56,7 +56,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method) assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL) assert.Equal(t, newURL, config.URL)
@@ -93,7 +93,7 @@ func TestMergeConfig(t *testing.T) {
DodosCount: utils.ToPtr(uint(10)), DodosCount: utils.ToPtr(uint(10)),
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "GET", *config.Method, "Method should remain unchanged") assert.Equal(t, "GET", *config.Method, "Method should remain unchanged")
assert.Equal(t, newURL, config.URL, "URL should be updated") assert.Equal(t, newURL, config.URL, "URL should be updated")
@@ -127,7 +127,7 @@ func TestMergeConfig(t *testing.T) {
} }
originalConfigCopy := *config originalConfigCopy := *config
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, originalConfigCopy.Method, config.Method) assert.Equal(t, originalConfigCopy.Method, config.Method)
assert.Equal(t, originalConfigCopy.URL, config.URL) assert.Equal(t, originalConfigCopy.URL, config.URL)
@@ -157,7 +157,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override") assert.Equal(t, []types.ConfigFile{*configFile}, config.Files, "Empty Files should not override")
assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override") assert.Equal(t, types.Params{{Key: "original", Value: []string{"value"}}}, config.Params, "Empty Params should not override")
@@ -182,7 +182,7 @@ func TestMergeConfig(t *testing.T) {
Method: utils.ToPtr("POST"), Method: utils.ToPtr("POST"),
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method, "Method should be updated") assert.Equal(t, "POST", *config.Method, "Method should be updated")
assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended") assert.Equal(t, []types.ConfigFile{*configFile1, *configFile2}, config.Files, "Files should be appended")
@@ -213,7 +213,7 @@ func TestMergeConfig(t *testing.T) {
Proxies: types.Proxies{}, Proxies: types.Proxies{},
} }
config.MergeConfig(newConfig) config.Merge(newConfig)
assert.Equal(t, "POST", *config.Method) assert.Equal(t, "POST", *config.Method)
assert.Equal(t, newURL, config.URL) assert.Equal(t, newURL, config.URL)

View File

@@ -0,0 +1,7 @@
package parser
import "github.com/aykhans/dodo/pkg/config"
type IParser interface {
Parse() (*config.Config, error)
}

View File

@@ -1,4 +1,4 @@
package config package parser
import ( import (
"errors" "errors"
@@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils" "github.com/aykhans/dodo/pkg/utils"
) )
@@ -52,6 +53,8 @@ Flags:
-x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080") -x, -proxy [string] Proxy for the request (e.g. "http://proxy.example.com:8080")
-skip-verify bool Skip SSL/TLS certificate verification (default %v)` -skip-verify bool Skip SSL/TLS certificate verification (default %v)`
var _ IParser = ConfigCLIParser{}
type ConfigCLIParser struct { type ConfigCLIParser struct {
args []string args []string
} }
@@ -79,13 +82,13 @@ func (arg *stringSliceArg) Set(value string) error {
// - types.ErrCLINoArgs // - types.ErrCLINoArgs
// - types.CLIUnexpectedArgsError // - types.CLIUnexpectedArgsError
// - types.FieldParseErrors // - types.FieldParseErrors
func (parser *ConfigCLIParser) Parse() (*Config, error) { func (parser ConfigCLIParser) Parse() (*config.Config, error) {
flagSet := flag.NewFlagSet("dodo", flag.ExitOnError) flagSet := flag.NewFlagSet("dodo", flag.ExitOnError)
flagSet.Usage = func() { parser.PrintHelp() } flagSet.Usage = func() { parser.PrintHelp() }
var ( var (
config = &Config{} config = &config.Config{}
configFiles = stringSliceArg{} configFiles = stringSliceArg{}
yes bool yes bool
skipVerify bool skipVerify bool
@@ -259,14 +262,14 @@ func (parser *ConfigCLIParser) Parse() (*Config, error) {
return config, nil return config, nil
} }
func (parser *ConfigCLIParser) PrintHelp() { func (parser ConfigCLIParser) PrintHelp() {
fmt.Printf( fmt.Printf(
cliUsageText+"\n", cliUsageText+"\n",
Defaults.Yes, config.Defaults.Yes,
Defaults.DodosCount, config.Defaults.DodosCount,
Defaults.RequestTimeout, config.Defaults.RequestTimeout,
Defaults.Method, config.Defaults.Method,
Defaults.SkipVerify, config.Defaults.SkipVerify,
) )
} }

View File

@@ -1,4 +1,4 @@
package config package parser
import ( import (
"bytes" "bytes"
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types" "github.com/aykhans/dodo/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -485,7 +486,7 @@ func TestConfigCLIParser_PrintHelp(t *testing.T) {
assert.Contains(t, output, "-f, -config-file") assert.Contains(t, output, "-f, -config-file")
// Verify default values are included // Verify default values are included
assert.Contains(t, output, Defaults.Method) assert.Contains(t, output, config.Defaults.Method)
assert.Contains(t, output, "1") // DodosCount default assert.Contains(t, output, "1") // DodosCount default
assert.Contains(t, output, "10s") // RequestTimeout default assert.Contains(t, output, "10s") // RequestTimeout default
assert.Contains(t, output, "false") // Yes default assert.Contains(t, output, "false") // Yes default

236
pkg/config/parser/env.go Normal file
View File

@@ -0,0 +1,236 @@
package parser
import (
"errors"
"fmt"
"net/url"
"os"
"time"
"github.com/aykhans/dodo/pkg/config"
"github.com/aykhans/dodo/pkg/types"
"github.com/aykhans/dodo/pkg/utils"
)
var _ IParser = ConfigENVParser{}
type ConfigENVParser struct {
envPrefix string
}
func NewConfigENVParser(envPrefix string) *ConfigENVParser {
return &ConfigENVParser{envPrefix}
}
// Parse parses env arguments into a Config object.
// It can return the following errors:
// - types.FieldParseErrors
func (parser ConfigENVParser) Parse() (*config.Config, error) {
var (
config = &config.Config{}
fieldParseErrors []types.FieldParseError
)
if configFile := parser.getEnv("CONFIG_FILE"); configFile != "" {
configFileParsed, err := types.ParseConfigFile(configFile)
_ = utils.HandleErrorOrDie(err,
utils.OnSentinelError(types.ErrConfigFileExtensionNotFound, func(err error) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
errors.New("file extension not found"),
),
)
return nil
}),
utils.OnCustomError(func(err types.RemoteConfigFileParseError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
fmt.Errorf("parse error: %w", err),
),
)
return nil
}),
utils.OnCustomError(func(err types.UnknownConfigFileTypeError) error {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("CONFIG_FILE"),
configFile,
fmt.Errorf("file type '%s' not supported (supported types: %s)", err.Type, types.ConfigFileTypeYAML),
),
)
return nil
}),
)
if err == nil {
config.Files = append(config.Files, *configFileParsed)
}
}
if yes := parser.getEnv("YES"); yes != "" {
yesParsed, err := utils.ParseString[bool](yes)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("YES"),
yes,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
),
)
} else {
config.Yes = &yesParsed
}
}
if skipVerify := parser.getEnv("SKIP_VERIFY"); skipVerify != "" {
skipVerifyParsed, err := utils.ParseString[bool](skipVerify)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("SKIP_VERIFY"),
skipVerify,
errors.New("invalid value for boolean, expected 'true' or 'false'"),
),
)
} else {
config.SkipVerify = &skipVerifyParsed
}
}
if method := parser.getEnv("METHOD"); method != "" {
config.Method = &method
}
if urlEnv := parser.getEnv("URL"); urlEnv != "" {
urlEnvParsed, err := url.Parse(urlEnv)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(parser.getFullEnvName("URL"), urlEnv, err),
)
} else {
config.URL = urlEnvParsed
}
}
if dodos := parser.getEnv("DODOS"); dodos != "" {
dodosParsed, err := utils.ParseString[uint](dodos)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("DODOS"),
dodos,
errors.New("invalid value for unsigned integer"),
),
)
} else {
config.DodosCount = &dodosParsed
}
}
if requests := parser.getEnv("REQUESTS"); requests != "" {
requestsParsed, err := utils.ParseString[uint](requests)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("REQUESTS"),
requests,
errors.New("invalid value for unsigned integer"),
),
)
} else {
config.RequestCount = &requestsParsed
}
}
if duration := parser.getEnv("DURATION"); duration != "" {
durationParsed, err := utils.ParseString[time.Duration](duration)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("DURATION"),
duration,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
),
)
} else {
config.Duration = &durationParsed
}
}
if timeout := parser.getEnv("TIMEOUT"); timeout != "" {
timeoutParsed, err := utils.ParseString[time.Duration](timeout)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("TIMEOUT"),
timeout,
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
),
)
} else {
config.Timeout = &timeoutParsed
}
}
if param := parser.getEnv("PARAM"); param != "" {
config.Params.Parse(param)
}
if header := parser.getEnv("HEADER"); header != "" {
config.Headers.Parse(header)
}
if cookie := parser.getEnv("COOKIE"); cookie != "" {
config.Cookies.Parse(cookie)
}
if body := parser.getEnv("BODY"); body != "" {
config.Bodies.Parse(body)
}
if proxy := parser.getEnv("PROXY"); proxy != "" {
err := config.Proxies.Parse(proxy)
if err != nil {
fieldParseErrors = append(
fieldParseErrors,
*types.NewFieldParseError(
parser.getFullEnvName("PROXY"),
proxy,
err,
),
)
}
}
if len(fieldParseErrors) > 0 {
return nil, types.NewFieldParseErrors(fieldParseErrors)
}
return config, nil
}
func (parser ConfigENVParser) getFullEnvName(envName string) string {
if parser.envPrefix == "" {
return envName
}
return parser.envPrefix + "_" + envName
}
func (parser ConfigENVParser) getEnv(envName string) string {
return os.Getenv(parser.getFullEnvName(envName))
}

File diff suppressed because it is too large Load Diff