MM-12359 Add 'command show' CLI command (#10073)
* MM-12359 Add 'command show' CLI command This new CLI command prints out the detailed information of a given slash command. * Fix typo in error returned when moving slash commands * Move prettyPrint CLI functions to util * Move prettyPrint CLI tests to util
Этот коммит содержится в:
коммит произвёл
Jesús Espino
родитель
179e98c245
Коммит
58ef6853f1
@@ -28,6 +28,15 @@ var CommandCreateCmd = &cobra.Command{
|
||||
RunE: createCommandCmdF,
|
||||
}
|
||||
|
||||
var CommandShowCmd = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Show a custom slash command",
|
||||
Long: `Show a custom slash command. Commands can be specified by command ID.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Example: ` command show commandID`,
|
||||
RunE: showCommandCmdF,
|
||||
}
|
||||
|
||||
var CommandMoveCmd = &cobra.Command{
|
||||
Use: "move",
|
||||
Short: "Move a slash command to a different team",
|
||||
@@ -71,6 +80,7 @@ func init() {
|
||||
|
||||
CommandCmd.AddCommand(
|
||||
CommandCreateCmd,
|
||||
CommandShowCmd,
|
||||
CommandMoveCmd,
|
||||
CommandListCmd,
|
||||
CommandDeleteCmd,
|
||||
@@ -148,6 +158,24 @@ func createCommandCmdF(command *cobra.Command, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func showCommandCmdF(command *cobra.Command, args []string) error {
|
||||
a, err := InitDBCommandContextCobra(command)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.Shutdown()
|
||||
|
||||
slashCommand := getCommandFromCommandArg(a, args[0])
|
||||
if slashCommand == nil {
|
||||
command.SilenceUsage = true
|
||||
return errors.New("Unable to find command '" + args[0] + "'")
|
||||
}
|
||||
// pretty print
|
||||
fmt.Printf("%s", prettyPrintStruct(*slashCommand))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func moveCommandCmdF(command *cobra.Command, args []string) error {
|
||||
a, err := InitDBCommandContextCobra(command)
|
||||
if err != nil {
|
||||
@@ -156,7 +184,7 @@ func moveCommandCmdF(command *cobra.Command, args []string) error {
|
||||
defer a.Shutdown()
|
||||
|
||||
if len(args) < 2 {
|
||||
return errors.New("Enter the destination team and at least one comamnd to move.")
|
||||
return errors.New("Enter the destination team and at least one command to move.")
|
||||
}
|
||||
|
||||
team := getTeamFromTeamArg(a, args[0])
|
||||
|
||||
@@ -131,6 +131,51 @@ func TestCreateCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowCommand(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
url := "http://localhost:8000/test-command"
|
||||
team := th.BasicTeam
|
||||
user := th.BasicUser
|
||||
th.LinkUserToTeam(user, team)
|
||||
trigger := "trigger_" + model.NewId()
|
||||
displayName := "dn_" + model.NewId()
|
||||
|
||||
c := &model.Command{
|
||||
DisplayName: displayName,
|
||||
Method: "G",
|
||||
TeamId: team.Id,
|
||||
Username: user.Username,
|
||||
CreatorId: user.Id,
|
||||
URL: url,
|
||||
Trigger: trigger,
|
||||
}
|
||||
|
||||
t.Run("existing command", func(t *testing.T) {
|
||||
command, err := th.App.CreateCommand(c)
|
||||
require.Nil(t, err)
|
||||
commands, err := th.App.ListTeamCommands(team.Id)
|
||||
require.Nil(t, err)
|
||||
assert.Equal(t, len(commands), 1)
|
||||
|
||||
output := th.CheckCommand(t, "command", "show", command.Id)
|
||||
assert.Contains(t, string(output), command.Id)
|
||||
assert.Contains(t, string(output), command.TeamId)
|
||||
assert.Contains(t, string(output), trigger)
|
||||
assert.Contains(t, string(output), displayName)
|
||||
assert.Contains(t, string(output), user.Username)
|
||||
})
|
||||
|
||||
t.Run("not existing command", func(t *testing.T) {
|
||||
assert.Error(t, th.RunCommand(t, "command", "show", "invalid"))
|
||||
})
|
||||
|
||||
t.Run("no commandID", func(t *testing.T) {
|
||||
assert.Error(t, th.RunCommand(t, "command", "show"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteCommand(t *testing.T) {
|
||||
// Skipped due to v5.6 RC build issues.
|
||||
t.Skip()
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
"github.com/mattermost/mattermost-server/model"
|
||||
"github.com/mattermost/mattermost-server/utils"
|
||||
"github.com/mattermost/mattermost-server/utils/fileutils"
|
||||
@@ -178,7 +176,7 @@ func configShowCmdF(command *cobra.Command, args []string) error {
|
||||
config := app.Config()
|
||||
|
||||
// pretty print
|
||||
fmt.Printf("%s", prettyPrint(configToMap(*config)))
|
||||
fmt.Printf("%s", prettyPrintStruct(*config))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -205,37 +203,6 @@ func printConfigValues(configMap map[string]interface{}, configSetting []string,
|
||||
}
|
||||
}
|
||||
|
||||
// prettyPrint the map
|
||||
func prettyPrint(configMap map[string]interface{}) string {
|
||||
value := reflect.ValueOf(configMap)
|
||||
return printMap(value, 0)
|
||||
}
|
||||
|
||||
// printMap takes a reflect.Value and print it out, recursively if its a map with the given tab settings.
|
||||
func printMap(value reflect.Value, tabVal int) string {
|
||||
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
for _, key := range value.MapKeys() {
|
||||
val := value.MapIndex(key)
|
||||
if newVal, ok := val.Interface().(map[string]interface{}); !ok {
|
||||
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
|
||||
fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface())
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
|
||||
fmt.Fprintf(out, "%v:\n", key.Interface())
|
||||
// going one level in, increase the tab
|
||||
tabVal++
|
||||
fmt.Fprintf(out, "%s", printMap(reflect.ValueOf(newVal), tabVal))
|
||||
// coming back one level, decrease the tab
|
||||
tabVal--
|
||||
}
|
||||
}
|
||||
|
||||
return out.String()
|
||||
|
||||
}
|
||||
|
||||
func configSetCmdF(command *cobra.Command, args []string) error {
|
||||
app, err := InitDBCommandContextCobra(command)
|
||||
if err != nil {
|
||||
@@ -388,44 +355,3 @@ func UpdateMap(configMap map[string]interface{}, configSettings []string, newVal
|
||||
func configToMap(s interface{}) map[string]interface{} {
|
||||
return structToMap(s)
|
||||
}
|
||||
|
||||
// structToMap converts a struct into a map
|
||||
func structToMap(t interface{}) map[string]interface{} {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
val := reflect.ValueOf(t)
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := map[string]interface{}{}
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
|
||||
var value interface{}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Struct:
|
||||
value = structToMap(field.Interface())
|
||||
case reflect.Ptr:
|
||||
indirectType := field.Elem()
|
||||
|
||||
if indirectType.Kind() == reflect.Struct {
|
||||
value = structToMap(indirectType.Interface())
|
||||
} else {
|
||||
value = indirectType.Interface()
|
||||
}
|
||||
default:
|
||||
value = field.Interface()
|
||||
}
|
||||
|
||||
out[val.Type().Field(i).Name] = value
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -151,93 +151,8 @@ func TestConfigSet(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestStructToMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input interface{}
|
||||
Expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
Name: "Struct with one string field",
|
||||
Input: struct {
|
||||
Test string
|
||||
}{
|
||||
Test: "test",
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"Test": "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "String with multiple fields of different ",
|
||||
Input: struct {
|
||||
Test1 string
|
||||
Test2 int
|
||||
Test3 string
|
||||
Test4 bool
|
||||
}{
|
||||
Test1: "test1",
|
||||
Test2: 21,
|
||||
Test3: "test2",
|
||||
Test4: false,
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"Test1": "test1",
|
||||
"Test2": 21,
|
||||
"Test3": "test2",
|
||||
"Test4": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Nested fields",
|
||||
Input: TestConfig{
|
||||
TestServiceSettings{"abc", "def", "ghi"},
|
||||
TestTeamSettings{"abc", 1},
|
||||
TestClientRequirements{"abc", "def", "ghi"},
|
||||
TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}},
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"TestServiceSettings": map[string]interface{}{
|
||||
"Siteurl": "abc",
|
||||
"Websocketurl": "def",
|
||||
"Licensedfieldlocation": "ghi",
|
||||
},
|
||||
"TestTeamSettings": map[string]interface{}{
|
||||
"Sitename": "abc",
|
||||
"Maxuserperteam": 1,
|
||||
},
|
||||
"TestClientRequirements": map[string]interface{}{
|
||||
"Androidlatestversion": "abc",
|
||||
"Androidminversion": "def",
|
||||
"Desktoplatestversion": "ghi",
|
||||
},
|
||||
"TestMessageExportSettings": map[string]interface{}{
|
||||
"Enableexport": true,
|
||||
"Exportformat": "abc",
|
||||
"TestGlobalRelaySettings": map[string]interface{}{
|
||||
"Customertype": "abc",
|
||||
"Smtpusername": "def",
|
||||
"Smtppassword": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res := structToMap(test.Input)
|
||||
|
||||
if !reflect.DeepEqual(res, test.Expected) {
|
||||
t.Errorf("got %v want %v ", res, test.Expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestConfigToMap(t *testing.T) {
|
||||
// This test is almost the same as TestMapToStruct, but I have it here for the sake of completions
|
||||
// This test is almost the same as TestStructToMap, but I have it here for the sake of completions
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input interface{}
|
||||
@@ -321,68 +236,6 @@ func TestConfigToMap(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintMap(t *testing.T) {
|
||||
inputCases := []interface{}{
|
||||
map[string]interface{}{
|
||||
"CustomerType": "A9",
|
||||
"SmtpUsername": "",
|
||||
"SmtpPassword": "",
|
||||
"EmailAddress": "",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"EnableExport": false,
|
||||
"ExportFormat": "actiance",
|
||||
"DailyRunTime": "01:00",
|
||||
"GlobalRelaySettings": map[string]interface{}{
|
||||
"CustomerType": "A9",
|
||||
"SmtpUsername": "",
|
||||
"SmtpPassword": "",
|
||||
"EmailAddress": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outputCases := []string{
|
||||
"CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n",
|
||||
"EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input reflect.Value
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Name: "Basic print",
|
||||
Input: reflect.ValueOf(inputCases[0]),
|
||||
Expected: outputCases[0],
|
||||
},
|
||||
{
|
||||
Name: "Complex print",
|
||||
Input: reflect.ValueOf(inputCases[1]),
|
||||
Expected: outputCases[1],
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res := printMap(test.Input, 0)
|
||||
|
||||
// create two slice of string formed by splitting our strings on \n
|
||||
slice1 := strings.Split(res, "\n")
|
||||
slice2 := strings.Split(res, "\n")
|
||||
|
||||
sort.Strings(slice1)
|
||||
sort.Strings(slice2)
|
||||
|
||||
if !reflect.DeepEqual(slice1, slice2) {
|
||||
t.Errorf("got '%#v' want '%#v", slice1, slice2)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintConfigValues(t *testing.T) {
|
||||
outputs := []string{
|
||||
"Siteurl: \"abc\"\nWebsocketurl: \"def\"\nLicensedfieldlocation: \"ghi\"\n",
|
||||
|
||||
89
cmd/mattermost/commands/utils.go
Обычный файл
89
cmd/mattermost/commands/utils.go
Обычный файл
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/mlog"
|
||||
)
|
||||
|
||||
// prettyPrintStruct will return a prettyPrint version of a given struct
|
||||
func prettyPrintStruct(t interface{}) string {
|
||||
return prettyPrintMap(structToMap(t))
|
||||
}
|
||||
|
||||
// structToMap converts a struct into a map
|
||||
func structToMap(t interface{}) map[string]interface{} {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
mlog.Error(fmt.Sprintf("Panicked in structToMap. This should never happen. %v", r))
|
||||
}
|
||||
}()
|
||||
|
||||
val := reflect.ValueOf(t)
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
|
||||
out := map[string]interface{}{}
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
|
||||
var value interface{}
|
||||
|
||||
switch field.Kind() {
|
||||
case reflect.Struct:
|
||||
value = structToMap(field.Interface())
|
||||
case reflect.Ptr:
|
||||
indirectType := field.Elem()
|
||||
|
||||
if indirectType.Kind() == reflect.Struct {
|
||||
value = structToMap(indirectType.Interface())
|
||||
} else {
|
||||
value = indirectType.Interface()
|
||||
}
|
||||
default:
|
||||
value = field.Interface()
|
||||
}
|
||||
|
||||
out[val.Type().Field(i).Name] = value
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// prettyPrintMap will return a prettyPrint version of a given map
|
||||
func prettyPrintMap(configMap map[string]interface{}) string {
|
||||
value := reflect.ValueOf(configMap)
|
||||
return printMap(value, 0)
|
||||
}
|
||||
|
||||
// printMap takes a reflect.Value and prints it out, recursively if it's a map with the given tab settings
|
||||
func printMap(value reflect.Value, tabVal int) string {
|
||||
out := &bytes.Buffer{}
|
||||
|
||||
for _, key := range value.MapKeys() {
|
||||
val := value.MapIndex(key)
|
||||
if newVal, ok := val.Interface().(map[string]interface{}); !ok {
|
||||
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
|
||||
fmt.Fprintf(out, "%v: \"%v\"\n", key.Interface(), val.Interface())
|
||||
} else {
|
||||
fmt.Fprintf(out, "%s", strings.Repeat("\t", tabVal))
|
||||
fmt.Fprintf(out, "%v:\n", key.Interface())
|
||||
// going one level in, increase the tab
|
||||
tabVal++
|
||||
fmt.Fprintf(out, "%s", printMap(reflect.ValueOf(newVal), tabVal))
|
||||
// coming back one level, decrease the tab
|
||||
tabVal--
|
||||
}
|
||||
}
|
||||
|
||||
return out.String()
|
||||
}
|
||||
157
cmd/mattermost/commands/utils_test.go
Обычный файл
157
cmd/mattermost/commands/utils_test.go
Обычный файл
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2019-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStructToMap(t *testing.T) {
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input interface{}
|
||||
Expected map[string]interface{}
|
||||
}{
|
||||
{
|
||||
Name: "Struct with one string field",
|
||||
Input: struct {
|
||||
Test string
|
||||
}{
|
||||
Test: "test",
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"Test": "test",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "String with multiple fields of different ",
|
||||
Input: struct {
|
||||
Test1 string
|
||||
Test2 int
|
||||
Test3 string
|
||||
Test4 bool
|
||||
}{
|
||||
Test1: "test1",
|
||||
Test2: 21,
|
||||
Test3: "test2",
|
||||
Test4: false,
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"Test1": "test1",
|
||||
"Test2": 21,
|
||||
"Test3": "test2",
|
||||
"Test4": false,
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Nested fields",
|
||||
Input: TestConfig{
|
||||
TestServiceSettings{"abc", "def", "ghi"},
|
||||
TestTeamSettings{"abc", 1},
|
||||
TestClientRequirements{"abc", "def", "ghi"},
|
||||
TestMessageExportSettings{true, "abc", TestGlobalRelaySettings{"abc", "def", "ghi"}},
|
||||
},
|
||||
Expected: map[string]interface{}{
|
||||
"TestServiceSettings": map[string]interface{}{
|
||||
"Siteurl": "abc",
|
||||
"Websocketurl": "def",
|
||||
"Licensedfieldlocation": "ghi",
|
||||
},
|
||||
"TestTeamSettings": map[string]interface{}{
|
||||
"Sitename": "abc",
|
||||
"Maxuserperteam": 1,
|
||||
},
|
||||
"TestClientRequirements": map[string]interface{}{
|
||||
"Androidlatestversion": "abc",
|
||||
"Androidminversion": "def",
|
||||
"Desktoplatestversion": "ghi",
|
||||
},
|
||||
"TestMessageExportSettings": map[string]interface{}{
|
||||
"Enableexport": true,
|
||||
"Exportformat": "abc",
|
||||
"TestGlobalRelaySettings": map[string]interface{}{
|
||||
"Customertype": "abc",
|
||||
"Smtpusername": "def",
|
||||
"Smtppassword": "ghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res := structToMap(test.Input)
|
||||
|
||||
if !reflect.DeepEqual(res, test.Expected) {
|
||||
t.Errorf("got %v want %v ", res, test.Expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintMap(t *testing.T) {
|
||||
inputCases := []interface{}{
|
||||
map[string]interface{}{
|
||||
"CustomerType": "A9",
|
||||
"SmtpUsername": "",
|
||||
"SmtpPassword": "",
|
||||
"EmailAddress": "",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"EnableExport": false,
|
||||
"ExportFormat": "actiance",
|
||||
"DailyRunTime": "01:00",
|
||||
"GlobalRelaySettings": map[string]interface{}{
|
||||
"CustomerType": "A9",
|
||||
"SmtpUsername": "",
|
||||
"SmtpPassword": "",
|
||||
"EmailAddress": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
outputCases := []string{
|
||||
"CustomerType: \"A9\"\nSmtpUsername: \"\"\nSmtpPassword: \"\"\nEmailAddress: \"\"\n",
|
||||
"EnableExport: \"false\"\nExportFormat: \"actiance\"\nDailyRunTime: \"01:00\"\nGlobalRelaySettings:\n\t CustomerType: \"A9\"\n\tSmtpUsername: \"\"\n\tSmtpPassword: \"\"\n\tEmailAddress: \"\"\n",
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
Name string
|
||||
Input reflect.Value
|
||||
Expected string
|
||||
}{
|
||||
{
|
||||
Name: "Basic print",
|
||||
Input: reflect.ValueOf(inputCases[0]),
|
||||
Expected: outputCases[0],
|
||||
},
|
||||
{
|
||||
Name: "Complex print",
|
||||
Input: reflect.ValueOf(inputCases[1]),
|
||||
Expected: outputCases[1],
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range cases {
|
||||
t.Run(test.Name, func(t *testing.T) {
|
||||
res := printMap(test.Input, 0)
|
||||
|
||||
// create two slice of string formed by splitting our strings on \n
|
||||
slice1 := strings.Split(res, "\n")
|
||||
slice2 := strings.Split(res, "\n")
|
||||
|
||||
sort.Strings(slice1)
|
||||
sort.Strings(slice2)
|
||||
|
||||
if !reflect.DeepEqual(slice1, slice2) {
|
||||
t.Errorf("got '%#v' want '%#v", slice1, slice2)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user