Skip to content

Commit e63c798

Browse files
umbynosper1234
andauthored
[breaking] Optimize core operations, improving on the user input (#1574)
* [breaking] remove `parseArch` var since it is always true * [breaking] make packages and platform case insensitive using the `core.GetPlatform()` approach * enhance comments and do not optimize if results are != 1 * add logging * add simple test, install, uninstall etc are already covered since they use the same piece of logic (`ParseReference()`) * Apply suggestions from code review Co-authored-by: per1234 <[email protected]> * add new error to handle multiple platform found, return res if the string the user is trying to operate matches perfectly one of the available platforms, optimize the code * enhance comment describing what the function does * add test to verify that an operation on two fake cores is not possible * skip test failing on macOS and on win and optimize the test Co-authored-by: per1234 <[email protected]>
1 parent 12adc53 commit e63c798

File tree

9 files changed

+145
-23
lines changed

9 files changed

+145
-23
lines changed

arduino/errors.go

+22
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package arduino
1717

1818
import (
1919
"fmt"
20+
"strings"
2021

2122
"github.com/arduino/arduino-cli/arduino/discovery"
2223
"github.com/arduino/arduino-cli/i18n"
@@ -715,3 +716,24 @@ func (e *SignatureVerificationFailedError) Unwrap() error {
715716
func (e *SignatureVerificationFailedError) ToRPCStatus() *status.Status {
716717
return status.New(codes.Unavailable, e.Error())
717718
}
719+
720+
// MultiplePlatformsError is returned when trying to detect
721+
// the Platform the user is trying to interact with and
722+
// and multiple results are found.
723+
type MultiplePlatformsError struct {
724+
Platforms []string
725+
UserPlatform string
726+
}
727+
728+
func (e *MultiplePlatformsError) Error() string {
729+
return tr("Found %d platform for reference \"%s\":\n%s",
730+
len(e.Platforms),
731+
e.UserPlatform,
732+
strings.Join(e.Platforms, "\n"),
733+
)
734+
}
735+
736+
// ToRPCStatus converts the error into a *status.Status
737+
func (e *MultiplePlatformsError) ToRPCStatus() *status.Status {
738+
return status.New(codes.InvalidArgument, e.Error())
739+
}

cli/arguments/reference.go

+54-16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ package arguments
1818
import (
1919
"fmt"
2020
"strings"
21+
22+
"github.com/arduino/arduino-cli/arduino"
23+
"github.com/arduino/arduino-cli/cli/instance"
24+
"github.com/arduino/arduino-cli/commands/core"
25+
rpc "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
26+
"github.com/sirupsen/logrus"
2127
)
2228

2329
// Reference represents a reference item (core or library) passed to the CLI
@@ -37,10 +43,10 @@ func (r *Reference) String() string {
3743

3844
// ParseReferences is a convenient wrapper that operates on a slice of strings and
3945
// calls ParseReference for each of them. It returns at the first invalid argument.
40-
func ParseReferences(args []string, parseArch bool) ([]*Reference, error) {
46+
func ParseReferences(args []string) ([]*Reference, error) {
4147
ret := []*Reference{}
4248
for _, arg := range args {
43-
reference, err := ParseReference(arg, parseArch)
49+
reference, err := ParseReference(arg)
4450
if err != nil {
4551
return nil, err
4652
}
@@ -49,10 +55,13 @@ func ParseReferences(args []string, parseArch bool) ([]*Reference, error) {
4955
return ret, nil
5056
}
5157

52-
// ParseReference parses a string and returns a Reference object. If `parseArch` is passed,
53-
// the method also tries to parse the architecture bit, i.e. string must be in the form
54-
// "packager:arch@version", useful to represent a platform (or core) name.
55-
func ParseReference(arg string, parseArch bool) (*Reference, error) {
58+
// ParseReference parses a string and returns a Reference object.
59+
// It tries to infer the platform the user is asking for.
60+
// To achieve that, it tries to use github.com/arduino/arduino-cli/commands/core.GetPlatform
61+
// Note that the Reference is returned rightaway if the arg inserted by the user matches perfectly one in the response of core.GetPlatform
62+
// A MultiplePlatformsError is returned if the platform searched by the user matches multiple platforms
63+
func ParseReference(arg string) (*Reference, error) {
64+
logrus.Infof("Parsing reference %s", arg)
5665
ret := &Reference{}
5766
if arg == "" {
5867
return nil, fmt.Errorf(tr("invalid empty core argument"))
@@ -69,20 +78,49 @@ func ParseReference(arg string, parseArch bool) (*Reference, error) {
6978
ret.Version = toks[1]
7079
}
7180

72-
if parseArch {
73-
toks = strings.Split(ret.PackageName, ":")
74-
if len(toks) != 2 {
75-
return nil, fmt.Errorf(tr("invalid item %s"), arg)
81+
toks = strings.Split(ret.PackageName, ":")
82+
if len(toks) != 2 {
83+
return nil, fmt.Errorf(tr("invalid item %s"), arg)
84+
}
85+
if toks[0] == "" {
86+
return nil, fmt.Errorf(tr("invalid empty core name '%s'"), arg)
87+
}
88+
ret.PackageName = toks[0]
89+
if toks[1] == "" {
90+
return nil, fmt.Errorf(tr("invalid empty core architecture '%s'"), arg)
91+
}
92+
ret.Architecture = toks[1]
93+
94+
// Now that we have the required informations in `ret` we can
95+
// try to use core.GetPlatforms to optimize what the user typed
96+
// (by replacing the PackageName and Architecture in ret with the content of core.GetPlatform())
97+
platforms, _ := core.GetPlatforms(&rpc.PlatformListRequest{
98+
Instance: instance.CreateAndInit(),
99+
UpdatableOnly: false,
100+
All: true, // this is true because we want also the installable platforms
101+
})
102+
foundPlatforms := []string{}
103+
for _, platform := range platforms {
104+
platformID := platform.GetId()
105+
platformUser := ret.PackageName + ":" + ret.Architecture
106+
// At first we check if the platform the user is searching for matches an available one,
107+
// this way we do not need to adapt the casing and we can return it directly
108+
if platformUser == platformID {
109+
return ret, nil
76110
}
77-
if toks[0] == "" {
78-
return nil, fmt.Errorf(tr("invalid empty core name '%s'"), arg)
111+
if strings.EqualFold(platformUser, platformID) {
112+
logrus.Infof("Found possible match for reference %s -> %s", platformUser, platformID)
113+
toks = strings.Split(platformID, ":")
114+
foundPlatforms = append(foundPlatforms, platformID)
79115
}
116+
}
117+
// replace the returned Reference only if only one occurrence is found,
118+
// otherwise return an error to the user because we don't know on which platform operate
119+
if len(foundPlatforms) == 1 {
80120
ret.PackageName = toks[0]
81-
if toks[1] == "" {
82-
return nil, fmt.Errorf(tr("invalid empty core architecture '%s'"), arg)
83-
}
84121
ret.Architecture = toks[1]
122+
} else {
123+
return nil, &arduino.MultiplePlatformsError{Platforms: foundPlatforms, UserPlatform: arg}
85124
}
86-
87125
return ret, nil
88126
}

cli/arguments/reference_test.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"testing"
2020

2121
"github.com/arduino/arduino-cli/cli/arguments"
22+
"github.com/arduino/arduino-cli/configuration"
2223
"github.com/stretchr/testify/assert"
2324
"github.com/stretchr/testify/require"
2425
)
@@ -45,6 +46,10 @@ var badCores = []struct {
4546
{"", nil},
4647
}
4748

49+
func init() {
50+
configuration.Settings = configuration.Init("")
51+
}
52+
4853
func TestArgsStringify(t *testing.T) {
4954
for _, core := range goodCores {
5055
require.Equal(t, core.in, core.expected.String())
@@ -53,13 +58,13 @@ func TestArgsStringify(t *testing.T) {
5358

5459
func TestParseReferenceCores(t *testing.T) {
5560
for _, tt := range goodCores {
56-
actual, err := arguments.ParseReference(tt.in, true)
61+
actual, err := arguments.ParseReference(tt.in)
5762
assert.Nil(t, err)
5863
assert.Equal(t, tt.expected, actual)
5964
}
6065

6166
for _, tt := range badCores {
62-
actual, err := arguments.ParseReference(tt.in, true)
67+
actual, err := arguments.ParseReference(tt.in)
6368
require.NotNil(t, err, "Testing bad core '%s'", tt.in)
6469
require.Equal(t, tt.expected, actual, "Testing bad core '%s'", tt.in)
6570
}
@@ -71,7 +76,7 @@ func TestParseArgs(t *testing.T) {
7176
input = append(input, tt.in)
7277
}
7378

74-
refs, err := arguments.ParseReferences(input, true)
79+
refs, err := arguments.ParseReferences(input)
7580
assert.Nil(t, err)
7681
assert.Equal(t, len(goodCores), len(refs))
7782

cli/core/download.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func runDownloadCommand(cmd *cobra.Command, args []string) {
5353

5454
logrus.Info("Executing `arduino-cli core download`")
5555

56-
platformsRefs, err := arguments.ParseReferences(args, true)
56+
platformsRefs, err := arguments.ParseReferences(args)
5757
if err != nil {
5858
feedback.Errorf(tr("Invalid argument passed: %v"), err)
5959
os.Exit(errorcodes.ErrBadArgument)

cli/core/install.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func runInstallCommand(cmd *cobra.Command, args []string) {
6161
inst := instance.CreateAndInit()
6262
logrus.Info("Executing `arduino-cli core install`")
6363

64-
platformsRefs, err := arguments.ParseReferences(args, true)
64+
platformsRefs, err := arguments.ParseReferences(args)
6565
if err != nil {
6666
feedback.Errorf(tr("Invalid argument passed: %v"), err)
6767
os.Exit(errorcodes.ErrBadArgument)

cli/core/uninstall.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func runUninstallCommand(cmd *cobra.Command, args []string) {
5050
inst := instance.CreateAndInit()
5151
logrus.Info("Executing `arduino-cli core uninstall`")
5252

53-
platformsRefs, err := arguments.ParseReferences(args, true)
53+
platformsRefs, err := arguments.ParseReferences(args)
5454
if err != nil {
5555
feedback.Errorf(tr("Invalid argument passed: %v"), err)
5656
os.Exit(errorcodes.ErrBadArgument)

cli/core/upgrade.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ func runUpgradeCommand(cmd *cobra.Command, args []string) {
7676

7777
// proceed upgrading, if anything is upgradable
7878
exitErr := false
79-
platformsRefs, err := arguments.ParseReferences(args, true)
79+
platformsRefs, err := arguments.ParseReferences(args)
8080
if err != nil {
8181
feedback.Errorf(tr("Invalid argument passed: %v"), err)
8282
os.Exit(errorcodes.ErrBadArgument)

docs/UPGRADING.md

+13
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
Here you can find a list of migration guides to handle breaking changes between releases of the CLI.
44

5+
## Unreleased
6+
7+
### `github.com/arduino/arduino-cli/cli/arguments.ParseReferences` function change
8+
9+
The `parseArch` parameter was removed since it was unused and was always true. This means that the architecture gets
10+
always parsed by the function.
11+
12+
### `github.com/arduino/arduino-cli/cli/arguments.ParseReference` function change
13+
14+
The `parseArch` parameter was removed since it was unused and was always true. This means that the architecture gets
15+
always parsed by the function. Furthermore the function now should also correctly interpret `packager:arch` spelled with
16+
the wrong casing.
17+
518
## 0.20.0
619

720
### `board details` arguments change

test/test_core.py

+44
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,10 @@ def test_core_download(run_command, downloads_dir):
240240
result = run_command(["core", "download", "bananas:avr"])
241241
assert result.failed
242242

243+
# Wrong casing
244+
result = run_command(["core", "download", "Arduino:[email protected]"])
245+
assert os.path.exists(os.path.join(downloads_dir, "packages", "core-ArduinoCore-samd-1.8.12.tar.bz2"))
246+
243247

244248
def _in(jsondata, name, version=None):
245249
installed_cores = json.loads(jsondata)
@@ -685,6 +689,46 @@ def test_core_list_platform_without_platform_txt(run_command, data_dir):
685689
assert core["name"] == "some-packager-some-arch"
686690

687691

692+
@pytest.mark.skipif(
693+
platform.system() in ["Darwin", "Windows"],
694+
reason="macOS by default is case insensitive https://github.com/actions/virtual-environments/issues/865 "
695+
+ "Windows too is case insensitive"
696+
+ "https://stackoverflow.com/questions/7199039/file-paths-in-windows-environment-not-case-sensitive",
697+
)
698+
def test_core_download_multiple_platforms(run_command, data_dir):
699+
assert run_command(["update"])
700+
701+
# Verifies no core is installed
702+
res = run_command(["core", "list", "--format", "json"])
703+
assert res.ok
704+
cores = json.loads(res.stdout)
705+
assert len(cores) == 0
706+
707+
# Simulates creation of two new cores in the sketchbook hardware folder
708+
test_boards_txt = Path(__file__).parent / "testdata" / "boards.local.txt"
709+
boards_txt = Path(data_dir, "packages", "PACKAGER", "hardware", "ARCH", "1.0.0", "boards.txt")
710+
boards_txt.parent.mkdir(parents=True, exist_ok=True)
711+
boards_txt.touch()
712+
assert boards_txt.write_bytes(test_boards_txt.read_bytes())
713+
714+
boards_txt1 = Path(data_dir, "packages", "packager", "hardware", "arch", "1.0.0", "boards.txt")
715+
boards_txt1.parent.mkdir(parents=True, exist_ok=True)
716+
boards_txt1.touch()
717+
assert boards_txt1.write_bytes(test_boards_txt.read_bytes())
718+
719+
# Verifies the two cores are detected
720+
res = run_command(["core", "list", "--format", "json"])
721+
assert res.ok
722+
cores = json.loads(res.stdout)
723+
assert len(cores) == 2
724+
725+
# Try to do an operation on the fake cores.
726+
# The cli should not allow it since optimizing the casing results in finding two cores
727+
res = run_command(["core", "upgrade", "Packager:Arch"])
728+
assert res.failed
729+
assert "Invalid argument passed: Found 2 platform for reference" in res.stderr
730+
731+
688732
def test_core_with_wrong_custom_board_options_is_loaded(run_command, data_dir):
689733
test_platform_name = "platform_with_wrong_custom_board_options"
690734
platform_install_dir = Path(data_dir, "hardware", "arduino-beta-dev", test_platform_name)

0 commit comments

Comments
 (0)