From dd99a67bf3096f0d46f25e5883e82296510813c6 Mon Sep 17 00:00:00 2001 From: Starnop Date: Fri, 25 Oct 2019 12:47:30 +0800 Subject: [PATCH] feature: add weight for node config Signed-off-by: Starnop --- cmd/dfget/app/root.go | 52 +++--- cmd/dfget/app/root_test.go | 12 +- dfget/config/config.go | 27 ++- dfget/config/config_test.go | 22 ++- dfget/config/constants.go | 1 + dfget/config/supernode_value.go | 227 ++++++++++++++++++++++++++ dfget/config/supernode_value_test.go | 100 ++++++++++++ docs/config/dfget_config_template.yml | 5 +- docs/config/dfget_properties.md | 2 +- pkg/algorithm/algorithm.go | 38 +++++ pkg/algorithm/algorithm_test.go | 66 ++++++++ 11 files changed, 498 insertions(+), 54 deletions(-) create mode 100644 dfget/config/supernode_value.go create mode 100644 dfget/config/supernode_value_test.go create mode 100644 pkg/algorithm/algorithm.go create mode 100644 pkg/algorithm/algorithm_test.go diff --git a/cmd/dfget/app/root.go b/cmd/dfget/app/root.go index 23a40346c..255ab6efc 100644 --- a/cmd/dfget/app/root.go +++ b/cmd/dfget/app/root.go @@ -72,7 +72,10 @@ func init() { // runDfget does some init operations and starts to download. func runDfget() error { // get config from property files - propResults := initProperties() + propResults, err := initProperties() + if err != nil { + return err + } // initialize logger if err := initClientLog(); err != nil { @@ -91,10 +94,6 @@ func runDfget() error { cfg.Filter = transFilter(filter) - if err := handleNodes(); err != nil { - return err - } - if err := checkParameters(); err != nil { return err } @@ -106,10 +105,10 @@ func runDfget() error { logrus.Infof("get init config:%v", cfg) // enter the core process - err := core.Start(cfg) - printer.Println(resultMsg(cfg, time.Now(), err)) - if err != nil { - os.Exit(err.Code) + dfError := core.Start(cfg) + printer.Println(resultMsg(cfg, time.Now(), dfError)) + if dfError != nil { + os.Exit(dfError.Code) } return nil } @@ -122,12 +121,12 @@ func checkParameters() error { } // initProperties loads config from property files. -func initProperties() []*propertiesResult { +func initProperties() ([]*propertiesResult, error) { var results []*propertiesResult properties := config.NewProperties() for _, v := range cfg.ConfigFiles { - var err error - if err = properties.Load(v); err == nil { + err := properties.Load(v) + if err == nil { break } results = append(results, &propertiesResult{ @@ -137,8 +136,12 @@ func initProperties() []*propertiesResult { }) } - if cfg.Nodes == nil { - cfg.Nodes = properties.Nodes + supernodes := cfg.Supernodes + if supernodes == nil { + supernodes = properties.Supernodes + } + if supernodes != nil { + cfg.Nodes = config.NodeWightSlice2StringSlice(supernodes) } if cfg.LocalLimit == 0 { @@ -173,7 +176,7 @@ func initProperties() []*propertiesResult { cfg.RV.SystemDataDir = path.Join(cfg.WorkHome, "data") cfg.RV.FileLength = -1 - return results + return results, nil } // initClientLog initializes dfget client's logger. @@ -234,8 +237,8 @@ func initFlags() { "\nin this way, different but actually the same URLs can reuse the same downloading task") flagSet.StringSliceVar(&cfg.Header, "header", nil, "http header, eg: --header='Accept: *' --header='Host: abc'") - flagSet.StringSliceVarP(&cfg.Nodes, "node", "n", nil, - "specify the addresses(host:port) of supernodes") + flagSet.VarP(config.NewSupernodesValue(&cfg.Supernodes, nil), "node", "n", + "specify the addresses(host:port=weight) of supernodes where the host is necessary, the port(default: 8002) and the weight(default:1) are optional. And the type of weight must be integer") flagSet.BoolVar(&cfg.Notbs, "notbs", false, "disable back source downloading for requested file when p2p fails to download it") flagSet.BoolVar(&cfg.DFDaemon, "dfdaemon", false, @@ -275,21 +278,6 @@ func transFilter(filter string) []string { return strings.Split(filter, "&") } -func handleNodes() error { - nodes := make([]string, 0) - - for _, v := range cfg.Nodes { - // TODO: check the validity of v. - if strings.IndexByte(v, ':') > 0 { - nodes = append(nodes, v) - continue - } - nodes = append(nodes, fmt.Sprintf("%s:%d", v, config.DefaultSupernodePort)) - } - cfg.Nodes = nodes - return nil -} - func resultMsg(cfg *config.Config, end time.Time, e *errortypes.DfError) string { if e != nil { return fmt.Sprintf("download FAIL(%d) cost:%.3fs length:%d reason:%d error:%v", diff --git a/cmd/dfget/app/root_test.go b/cmd/dfget/app/root_test.go index 6bfd4de8c..5cad19ce1 100644 --- a/cmd/dfget/app/root_test.go +++ b/cmd/dfget/app/root_test.go @@ -37,7 +37,7 @@ type dfgetSuit struct { func (suit *dfgetSuit) Test_initFlagsNoArguments() { initProperties() - suit.Equal(cfg.Nodes, []string{"127.0.0.1"}) + suit.Equal(cfg.Nodes, []string{"127.0.0.1:8002"}) suit.Equal(cfg.LocalLimit, 20*rate.MB) suit.Equal(cfg.TotalLimit, 20*rate.MB) suit.Equal(cfg.Notbs, false) @@ -69,11 +69,11 @@ func (suit *dfgetSuit) Test_initProperties() { {configs: nil, expected: config.NewProperties()}, {configs: []string{iniFile, yamlFile}, - expected: newProp(0, 0, 0, "1.1.1.1")}, + expected: newProp(0, 0, 0, "1.1.1.1:8002")}, {configs: []string{yamlFile, iniFile}, - expected: newProp(int(rate.KB*1000), int(rate.KB*1000), 0, "1.1.1.2")}, + expected: newProp(int(rate.KB*1000), int(rate.KB*1000), 0, "1.1.1.2:8002")}, {configs: []string{filepath.Join(dirName, "x"), yamlFile}, - expected: newProp(int(rate.KB*1000), int(rate.KB*1000), 0, "1.1.1.2")}, + expected: newProp(int(rate.KB*1000), int(rate.KB*1000), 0, "1.1.1.2:8002")}, } for _, v := range cases { @@ -84,7 +84,7 @@ func (suit *dfgetSuit) Test_initProperties() { "--locallimit", v.expected.LocalLimit.String(), "--totallimit", v.expected.TotalLimit.String()}) initProperties() - suit.EqualValues(cfg.Nodes, v.expected.Nodes) + suit.EqualValues(cfg.Nodes, config.NodeWightSlice2StringSlice(v.expected.Supernodes)) suit.Equal(cfg.LocalLimit, v.expected.LocalLimit) suit.Equal(cfg.TotalLimit, v.expected.TotalLimit) suit.Equal(cfg.ClientQueueSize, v.expected.ClientQueueSize) @@ -134,7 +134,7 @@ func TestSuite(t *testing.T) { func newProp(local int, total int, size int, nodes ...string) *config.Properties { p := config.NewProperties() if nodes != nil { - p.Nodes = nodes + p.Supernodes, _ = config.ParseNodesSlice(nodes) } if local != 0 { p.LocalLimit = rate.Rate(local) diff --git a/dfget/config/config.go b/dfget/config/config.go index 81591c9b6..2ff03e350 100644 --- a/dfget/config/config.go +++ b/dfget/config/config.go @@ -33,7 +33,6 @@ import ( "github.com/dragonflyoss/Dragonfly/pkg/printer" "github.com/dragonflyoss/Dragonfly/pkg/rate" "github.com/dragonflyoss/Dragonfly/pkg/stringutils" - "github.com/pkg/errors" "gopkg.in/gcfg.v1" "gopkg.in/warnings.v0" @@ -50,14 +49,18 @@ import ( // Since 0.2.0, the INI config is just to be compatible with previous versions. // The YAML config will have more properties: // nodes: -// - 127.0.0.1 -// - 10.10.10.1 +// - 127.0.0.1=1 +// - 10.10.10.1:8002=2 // localLimit: 20M // totalLimit: 20M // clientQueueSize: 6 type Properties struct { - // Nodes specify supernodes. - Nodes []string `yaml:"nodes,omitempty" json:"nodes,omitempty"` + // Supernodes specify supernodes with weight. + // The type of weight must be integer. + // All weights will be divided by the greatest common divisor in the end. + // + // E.g. ["192.168.33.21=1", "192.168.33.22=2"] + Supernodes []*NodeWight `yaml:"nodes,omitempty" json:"nodes,omitempty"` // LocalLimit rate limit about a single download task, format: G(B)/g/M(B)/m/K(B)/k/B // pure number will also be parsed as Byte. @@ -85,7 +88,7 @@ type Properties struct { // NewProperties creates a new properties with default values. func NewProperties() *Properties { return &Properties{ - Nodes: []string{DefaultNode}, + Supernodes: GetDefaultSupernodesValue(), LocalLimit: DefaultLocalLimit, MinRate: DefaultMinRate, ClientQueueSize: DefaultClientQueueSize, @@ -123,8 +126,13 @@ func (p *Properties) loadFromIni(path string) error { return fmt.Errorf("read ini config from %s error: %v", path, err) } } - p.Nodes = strings.Split(oldConfig.Node.Address, ",") - return nil + + nodes, err := ParseNodesString(oldConfig.Node.Address) + if err != nil { + return errors.Wrapf(err, "failed to handle nodes") + } + p.Supernodes = nodes + return err } func (p *Properties) fileType(path string) string { @@ -197,6 +205,9 @@ type Config struct { // If set true, log level will be 'debug'. Verbose bool `json:"verbose,omitempty"` + // Nodes specify supernodes. + Nodes []string `json:"-"` + // Start time. StartTime time.Time `json:"-"` diff --git a/dfget/config/config_test.go b/dfget/config/config_test.go index a03cd57ec..4ff178a75 100644 --- a/dfget/config/config_test.go +++ b/dfget/config/config_test.go @@ -173,19 +173,31 @@ func (suite *ConfigSuite) TestProperties_Load(c *check.C) { content: "nodes:\n\t- 10.10.10.1", errMsg: "yaml", expected: nil}, {create: true, ext: "yaml", content: "nodes:\n - 10.10.10.1\n - 10.10.10.2\n", - errMsg: "", expected: &Properties{Nodes: []string{"10.10.10.1", "10.10.10.2"}}}, + errMsg: "", expected: &Properties{Supernodes: []*NodeWight{ + {"10.10.10.1:8002", 1}, + {"10.10.10.2:8002", 1}, + }}}, {create: true, ext: "yaml", content: "totalLimit: 10M", errMsg: "", expected: &Properties{TotalLimit: 10 * rate.MB}}, {create: false, ext: "ini", content: "[node]\naddress=1.1.1.1", errMsg: "read ini config"}, {create: true, ext: "ini", content: "[node]\naddress=1.1.1.1", - expected: &Properties{Nodes: []string{"1.1.1.1"}}}, + expected: &Properties{Supernodes: []*NodeWight{ + {"1.1.1.1:8002", 1}, + }}}, {create: true, ext: "conf", content: "[node]\naddress=1.1.1.1", - expected: &Properties{Nodes: []string{"1.1.1.1"}}}, + expected: &Properties{Supernodes: []*NodeWight{ + {"1.1.1.1:8002", 1}, + }}}, {create: true, ext: "conf", content: "[node]\naddress=1.1.1.1,1.1.1.2", - expected: &Properties{Nodes: []string{"1.1.1.1", "1.1.1.2"}}}, + expected: &Properties{Supernodes: []*NodeWight{ + {"1.1.1.1:8002", 1}, + {"1.1.1.2:8002", 1}, + }}}, {create: true, ext: "conf", content: "[node]\naddress=1.1.1.1\n[totalLimit]", - expected: &Properties{Nodes: []string{"1.1.1.1"}}}, + expected: &Properties{Supernodes: []*NodeWight{ + {"1.1.1.1:8002", 1}, + }}}, } for idx, v := range cases { diff --git a/dfget/config/constants.go b/dfget/config/constants.go index 9d62b011f..c062a66cc 100644 --- a/dfget/config/constants.go +++ b/dfget/config/constants.go @@ -54,6 +54,7 @@ const ( DefaultMinRate = 64 * rate.KB DefaultTotalLimit = 20 * rate.MB DefaultClientQueueSize = 6 + DefaultSupernodeWeight = 1 ) /* http headers */ diff --git a/dfget/config/supernode_value.go b/dfget/config/supernode_value.go new file mode 100644 index 000000000..3b2970a30 --- /dev/null +++ b/dfget/config/supernode_value.go @@ -0,0 +1,227 @@ +/* + * Copyright The Dragonfly Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/dragonflyoss/Dragonfly/pkg/algorithm" + "github.com/dragonflyoss/Dragonfly/pkg/errortypes" + + "github.com/pkg/errors" +) + +const weightSeparator = '=' + +type SupernodesValue struct { + Nodes *[]*NodeWight +} + +type NodeWight struct { + Node string + Weight int +} + +func NewSupernodesValue(p *[]*NodeWight, val []*NodeWight) *SupernodesValue { + ssv := new(SupernodesValue) + ssv.Nodes = p + *ssv.Nodes = val + return ssv +} + +// GetDefaultSupernodesValue returns the default value of supernodes. +// default: ["127.0.0.1:8002=1"] +func GetDefaultSupernodesValue() []*NodeWight { + var result = make([]*NodeWight, 0) + result = append(result, &NodeWight{ + Node: fmt.Sprintf("%s:%d", DefaultNode, DefaultSupernodePort), + Weight: DefaultSupernodeWeight, + }) + return result +} + +// String implements the pflag.Value interface. +func (sv *SupernodesValue) String() string { + var result []string + for _, v := range *sv.Nodes { + result = append(result, v.string()) + } + return strings.Join(result, ",") +} + +// Set implements the pflag.Value interface. +func (sv *SupernodesValue) Set(value string) error { + nodes, err := ParseNodesString(value) + if err != nil { + return err + } + + *sv.Nodes = nodes + return nil +} + +// Type implements the pflag.Value interface. +func (sv *SupernodesValue) Type() string { + return "supernodes" +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (nw *NodeWight) MarshalYAML() (interface{}, error) { + return nw.string(), nil +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (nw *NodeWight) UnmarshalYAML(unmarshal func(interface{}) error) error { + var value string + if err := unmarshal(&value); err != nil { + return err + } + + nodeWeight, err := string2NodeWeight(value) + if err != nil { + return err + } + + *nw = *nodeWeight + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (nw *NodeWight) MarshalJSON() ([]byte, error) { + return json.Marshal(nw.string()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (nw *NodeWight) UnmarshalJSON(b []byte) error { + str, _ := strconv.Unquote(string(b)) + nodeWeight, err := string2NodeWeight(str) + if err != nil { + return err + } + + *nw = *nodeWeight + return nil +} + +func (nw *NodeWight) string() string { + return fmt.Sprintf("%s%c%d", nw.Node, weightSeparator, nw.Weight) +} + +// ParseNodesString parses the value in string type to []*NodeWight. +func ParseNodesString(value string) ([]*NodeWight, error) { + return ParseNodesSlice(strings.Split(value, ",")) +} + +// ParseNodesString parses the value in string slice type to []*NodeWight. +func ParseNodesSlice(value []string) ([]*NodeWight, error) { + nodeWightSlice := make([]*NodeWight, 0) + weightKey := make([]int, 0) + + // split node and weight + for _, v := range value { + nodeWeight, err := string2NodeWeight(v) + if err != nil { + return nil, errors.Wrapf(errortypes.ErrInvalidValue, "node: %s %v", v, err) + } + + weightKey = append(weightKey, nodeWeight.Weight) + nodeWightSlice = append(nodeWightSlice, nodeWeight) + } + + var result []*NodeWight + // get the the greatest common divisor of the weight slice and + // divide all weights by the greatest common divisor. + gcdNumber := algorithm.GCDSlice(weightKey) + for _, v := range nodeWightSlice { + result = append(result, &NodeWight{ + Node: v.Node, + Weight: (v.Weight / gcdNumber), + }) + } + + return result, nil +} + +// NodeWightSlice2StringSlice parses nodeWight slice to string slice. +// It takes the NodeWight.Node as the value and every value will be appended the corresponding NodeWight.Weight times. +func NodeWightSlice2StringSlice(supernodes []*NodeWight) []string { + var nodes []string + for _, v := range supernodes { + for i := 0; i < v.Weight; i++ { + nodes = append(nodes, v.Node) + } + } + return nodes +} + +func string2NodeWeight(value string) (*NodeWight, error) { + node, weight, err := splitNodeAndWeight(value) + if err != nil { + return nil, err + } + + node, err = handleDefaultPort(node) + if err != nil { + return nil, err + } + + return &NodeWight{ + Node: node, + Weight: weight, + }, nil +} + +// splitNodeAndWeight returns the node address and weight which parsed by the given value. +// If no weight specified, the DefaultSupernodeWeight will be returned as the weight value. +func splitNodeAndWeight(value string) (string, int, error) { + result := strings.Split(value, string(weightSeparator)) + splitLength := len(result) + + switch splitLength { + case 1: + return result[0], DefaultSupernodeWeight, nil + case 2: + v, err := strconv.Atoi(result[1]) + if err != nil { + return "", 0, err + } + return result[0], v, nil + default: + return "", 0, errortypes.ErrInvalidValue + } +} + +func handleDefaultPort(node string) (string, error) { + result := strings.Split(node, ":") + splitLength := len(result) + + if splitLength == 2 { + if result[0] == "" || result[1] == "" { + return "", errortypes.ErrInvalidValue + } + return node, nil + } + + if splitLength == 1 { + return fmt.Sprintf("%s:%d", node, DefaultSupernodePort), nil + } + + return "", errortypes.ErrInvalidValue +} diff --git a/dfget/config/supernode_value_test.go b/dfget/config/supernode_value_test.go new file mode 100644 index 000000000..c108e35f8 --- /dev/null +++ b/dfget/config/supernode_value_test.go @@ -0,0 +1,100 @@ +/* + * Copyright The Dragonfly Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestSuite(t *testing.T) { + suite.Run(t, new(SupernodeValueSuite)) +} + +type SupernodeValueSuite struct { + suite.Suite +} + +func (suit *SupernodeValueSuite) TestHandleNodes() { + var cases = []struct { + nodeWithWeightList []string + expectedNodes []*NodeWight + gotError bool + }{ + { + nodeWithWeightList: []string{"127.0.0.1", "127.0.0.2"}, + expectedNodes: []*NodeWight{ + {"127.0.0.1:8002", 1}, + {"127.0.0.2:8002", 1}, + }, + }, + { + nodeWithWeightList: []string{"127.0.0.1=2", "127.0.0.2"}, + expectedNodes: []*NodeWight{ + {"127.0.0.1:8002", 2}, + {"127.0.0.2:8002", 1}, + }, + }, + { + nodeWithWeightList: []string{"127.0.0.1=20", "127.0.0.2=20"}, + expectedNodes: []*NodeWight{ + {"127.0.0.1:8002", 1}, + {"127.0.0.2:8002", 1}}, + }, + { + nodeWithWeightList: []string{"127.0.0.1=2", "127.0.0.2=4"}, + expectedNodes: []*NodeWight{ + {"127.0.0.1:8002", 1}, + {"127.0.0.2:8002", 2}}, + }, + { + nodeWithWeightList: []string{"127.0.0.1:8002=1", "127.0.0.2:8001=2"}, + expectedNodes: []*NodeWight{ + {"127.0.0.1:8002", 1}, + {"127.0.0.2:8001", 2}}, + }, + { + nodeWithWeightList: []string{"127.0.0.1:=2"}, + gotError: true, + }, + { + nodeWithWeightList: []string{"127.0.0.1==1"}, + expectedNodes: nil, + gotError: true, + }, + { + nodeWithWeightList: []string{"==2"}, + expectedNodes: nil, + gotError: true, + }, + { + nodeWithWeightList: []string{"127.0.0.1==2"}, + expectedNodes: nil, + gotError: true, + }, + } + + for _, v := range cases { + nodes, err := ParseNodesSlice(v.nodeWithWeightList) + if v.gotError { + suit.NotNil(err) + } else { + suit.Equal(v.expectedNodes, nodes) + } + } +} diff --git a/docs/config/dfget_config_template.yml b/docs/config/dfget_config_template.yml index 2f38d5c4c..4a83699fa 100644 --- a/docs/config/dfget_config_template.yml +++ b/docs/config/dfget_config_template.yml @@ -2,9 +2,10 @@ # You can configure your dfget by change the parameter according your requirement. # Nodes specify supernodes. +# Where the host is necessary, the port(default: 8002) and the weight(default:1) are optional. nodes: -  - 127.0.0.1 -  - 10.10.10.1 +  - 127.0.0.1=1 +  - 10.10.10.1:8002=2 # LocalLimit rate limit about a single download task, format: G(B)/g/M(B)/m/K(B)/k/B # pure number will also be parsed as Byte. diff --git a/docs/config/dfget_properties.md b/docs/config/dfget_properties.md index 03eb31e0f..b2f72e702 100644 --- a/docs/config/dfget_properties.md +++ b/docs/config/dfget_properties.md @@ -8,7 +8,7 @@ The following startup parameters are supported for `dfget` | Parameter | Description | | ------------- | ------------- | -| nodes | Nodes specify supernodes | +| nodes | Nodes specify supernodes with format host:port=weight where the host is necessary, the port(default: 8002) and the weight(default:1) are optional. | | localLimit | LocalLimit rate limit about a single download task,format: 20M/m/K/k | | minRate | Minimal rate about a single download task. it's type is integer. The format of `M/m/K/k` will be supported soon | | totalLimit | TotalLimit rate limit about the whole host,format: 20M/m/K/k | diff --git a/pkg/algorithm/algorithm.go b/pkg/algorithm/algorithm.go new file mode 100644 index 000000000..942f5a93d --- /dev/null +++ b/pkg/algorithm/algorithm.go @@ -0,0 +1,38 @@ +/* + * Copyright The Dragonfly Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package algorithm + +// GCDSlice returns the greatest common divisor of a slice. +func GCDSlice(s []int) int { + length := len(s) + if length == 1 { + return s[0] + } + + return GCD(s[length-1], GCDSlice(s[:length-1])) +} + +// GCD returns the greatest common divisor of x and y. +func GCD(x, y int) int { + var z int + for y != 0 { + z = x % y + x = y + y = z + } + return x +} diff --git a/pkg/algorithm/algorithm_test.go b/pkg/algorithm/algorithm_test.go new file mode 100644 index 000000000..2eb808430 --- /dev/null +++ b/pkg/algorithm/algorithm_test.go @@ -0,0 +1,66 @@ +/* + * Copyright The Dragonfly Authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package algorithm + +import ( + "testing" + + "github.com/stretchr/testify/suite" +) + +func TestSuite(t *testing.T) { + suite.Run(t, new(AlgorithmSuite)) +} + +type AlgorithmSuite struct { + suite.Suite +} + +func (suit *AlgorithmSuite) TestGCD() { + var cases = []struct { + x int + y int + result int + }{ + {2, 2, 2}, + {4, 8, 4}, + {2, 5, 1}, + {10, 15, 5}, + } + + for _, v := range cases { + result := GCD(v.x, v.y) + suit.Equal(v.result, result) + } +} + +func (suit *AlgorithmSuite) TestGCDSlice() { + var cases = []struct { + slice []int + result int + }{ + {[]int{2, 2, 4}, 2}, + {[]int{5, 10, 25}, 5}, + {[]int{1, 3, 5}, 1}, + {[]int{66, 22, 33}, 11}, + } + + for _, v := range cases { + result := GCDSlice(v.slice) + suit.Equal(v.result, result) + } +}