Skip to content

Commit 5b3b4f1

Browse files
committed
Fix loading of sketches with folder and main file mismatched casing
1 parent a9b0c9d commit 5b3b4f1

File tree

14 files changed

+274
-7
lines changed

14 files changed

+274
-7
lines changed

arduino/builder/sketch_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,17 @@ func TestCopyAdditionalFiles(t *testing.T) {
230230
info2, err := os.Stat(s2.AdditionalFiles[0].Path)
231231
require.Equal(t, info1.ModTime(), info2.ModTime())
232232
}
233+
234+
func TestLoadSketchCaseMismatch(t *testing.T) {
235+
// pass the path to the sketch folder
236+
sketchPath := filepath.Join("testdata", t.Name())
237+
mainFilePath := filepath.Join(sketchPath, t.Name()+".ino")
238+
s, err := builder.SketchLoad(sketchPath, "")
239+
require.Nil(t, s)
240+
require.Error(t, err)
241+
242+
// pass the path to the main file
243+
s, err = builder.SketchLoad(mainFilePath, "")
244+
require.Nil(t, s)
245+
require.Error(t, err)
246+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}

arduino/sketch/sketch.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strings"
2323

2424
"github.com/arduino/arduino-cli/arduino/globals"
25+
"github.com/arduino/go-paths-helper"
2526
"github.com/pkg/errors"
2627
)
2728

@@ -116,10 +117,43 @@ func New(sketchFolderPath, mainFilePath, buildPath string, allFilesPaths []strin
116117
sort.Sort(ItemByPath(additionalFiles))
117118
sort.Sort(ItemByPath(otherSketchFiles))
118119

120+
if err := CheckSketchCasing(sketchFolderPath); err != nil {
121+
return nil, err
122+
}
123+
119124
return &Sketch{
120125
MainFile: mainFile,
121126
LocationPath: sketchFolderPath,
122127
OtherSketchFiles: otherSketchFiles,
123128
AdditionalFiles: additionalFiles,
124129
}, nil
125130
}
131+
132+
// CheckSketchCasing returns an error if the casing of the sketch folder and the main file are different.
133+
// Correct:
134+
// MySketch/MySketch.ino
135+
// Wrong:
136+
// MySketch/mysketch.ino
137+
// mysketch/MySketch.ino
138+
//
139+
// This is mostly necessary to avoid errors on Mac OS X.
140+
// For more info see: https://github.com/arduino/arduino-cli/issues/1174
141+
func CheckSketchCasing(sketchFolder string) error {
142+
sketchPath := paths.New(sketchFolder)
143+
files, err := sketchPath.ReadDir()
144+
if err != nil {
145+
return errors.Errorf("reading files: %v", err)
146+
}
147+
files.FilterOutDirs()
148+
149+
sketchName := sketchPath.Base()
150+
files.FilterPrefix(sketchName)
151+
152+
if files.Len() == 0 {
153+
sketchFolderPath := paths.New(sketchFolder)
154+
sketchFile := sketchFolderPath.Join(sketchFolderPath.Base() + globals.MainFileValidExtension)
155+
return errors.Errorf("no valid sketch found in %s: missing %s", sketchFolderPath, sketchFile)
156+
}
157+
158+
return nil
159+
}

arduino/sketch/sketch_test.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,15 @@
1616
package sketch_test
1717

1818
import (
19+
"fmt"
1920
"path/filepath"
2021
"sort"
2122
"testing"
2223

2324
"github.com/arduino/arduino-cli/arduino/sketch"
25+
"github.com/arduino/go-paths-helper"
2426
"github.com/stretchr/testify/assert"
27+
"github.com/stretchr/testify/require"
2528
)
2629

2730
func TestNewItem(t *testing.T) {
@@ -43,9 +46,9 @@ func TestNewItem(t *testing.T) {
4346

4447
func TestSort(t *testing.T) {
4548
items := []*sketch.Item{
46-
&sketch.Item{"foo"},
47-
&sketch.Item{"baz"},
48-
&sketch.Item{"bar"},
49+
{"foo"},
50+
{"baz"},
51+
{"bar"},
4952
}
5053

5154
sort.Sort(sketch.ItemByPath(items))
@@ -71,3 +74,37 @@ func TestNew(t *testing.T) {
7174
assert.Len(t, sketch.OtherSketchFiles, 0)
7275
assert.Len(t, sketch.AdditionalFiles, 1)
7376
}
77+
78+
func TestNewSketchCasingWrong(t *testing.T) {
79+
sketchPath := paths.New("testdata", "SketchCasingWrong")
80+
mainFilePath := paths.New("testadata", "sketchcasingwrong.ino").String()
81+
sketch, err := sketch.New(sketchPath.String(), mainFilePath, "", []string{mainFilePath})
82+
assert.Nil(t, sketch)
83+
expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchPath.String(), sketchPath.Join(sketchPath.Base()+".ino"))
84+
assert.EqualError(t, err, expectedError)
85+
}
86+
87+
func TestNewSketchCasingCorrect(t *testing.T) {
88+
sketchPath := paths.New("testdata", "SketchCasingCorrect").String()
89+
mainFilePath := paths.New("testadata", "SketchCasingCorrect.ino").String()
90+
sketch, err := sketch.New(sketchPath, mainFilePath, "", []string{mainFilePath})
91+
assert.NotNil(t, sketch)
92+
assert.NoError(t, err)
93+
assert.Equal(t, sketchPath, sketch.LocationPath)
94+
assert.Equal(t, mainFilePath, sketch.MainFile.Path)
95+
assert.Len(t, sketch.OtherSketchFiles, 0)
96+
assert.Len(t, sketch.AdditionalFiles, 0)
97+
}
98+
99+
func TestCheckSketchCasingWrong(t *testing.T) {
100+
sketchFolder := paths.New("testdata", "SketchCasingWrong")
101+
err := sketch.CheckSketchCasing(sketchFolder.String())
102+
expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolder, sketchFolder.Join(sketchFolder.Base()+".ino"))
103+
assert.EqualError(t, err, expectedError)
104+
}
105+
106+
func TestCheckSketchCasingCorrect(t *testing.T) {
107+
sketchFolder := paths.New("testdata", "SketchCasingCorrect").String()
108+
err := sketch.CheckSketchCasing(sketchFolder)
109+
require.NoError(t, err)
110+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}

arduino/sketches/sketches.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/arduino/arduino-cli/arduino/builder"
2323
"github.com/arduino/arduino-cli/arduino/globals"
24+
"github.com/arduino/arduino-cli/arduino/sketch"
2425
"github.com/arduino/go-paths-helper"
2526
"github.com/pkg/errors"
2627
)
@@ -70,19 +71,19 @@ func NewSketchFromPath(path *paths.Path) (*Sketch, error) {
7071
}
7172
}
7273

73-
if mainSketchFile == nil {
74+
if mainSketchFile == nil || sketch.CheckSketchCasing(path.String()) != nil {
7475
sketchFile := path.Join(path.Base() + globals.MainFileValidExtension)
7576
return nil, errors.Errorf("no valid sketch found in %s: missing %s", path, sketchFile)
7677
}
7778

78-
sketch := &Sketch{
79+
s := &Sketch{
7980
FullPath: path,
8081
MainFileExtension: mainSketchFile.Ext(),
8182
Name: path.Base(),
8283
Metadata: &Metadata{},
8384
}
84-
sketch.ImportMetadata()
85-
return sketch, nil
85+
s.ImportMetadata()
86+
return s, nil
8687
}
8788

8889
// ImportMetadata imports metadata into the sketch from a sketch.json file in the root

arduino/sketches/sketches_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,3 +109,24 @@ func TestCheckForPdeFiles(t *testing.T) {
109109
require.Len(t, files, 1)
110110
require.Equal(t, sketchPath.Parent().Join("SketchMultipleMainFiles.pde"), files[0])
111111
}
112+
113+
func TestSketchLoadWithCasing(t *testing.T) {
114+
sketchFolder := paths.New("testdata", "SketchCasingWrong")
115+
116+
sketch, err := NewSketchFromPath(sketchFolder)
117+
require.Nil(t, sketch)
118+
119+
sketchFolderAbs, _ := sketchFolder.Abs()
120+
sketchMainFileAbs := sketchFolderAbs.Join("SketchCasingWrong.ino")
121+
expectedError := fmt.Sprintf("no valid sketch found in %s: missing %s", sketchFolderAbs, sketchMainFileAbs)
122+
require.EqualError(t, err, expectedError)
123+
}
124+
125+
func TestSketchLoadingCorrectCasing(t *testing.T) {
126+
sketchFolder := paths.New("testdata", "SketchCasingCorrect")
127+
sketch, err := NewSketchFromPath(sketchFolder)
128+
require.NotNil(t, sketch)
129+
require.NoError(t, err)
130+
require.Equal(t, sketch.Name, "SketchCasingCorrect")
131+
require.True(t, sketch.FullPath.EquivalentTo(sketchFolder))
132+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
void setup() {
2+
3+
}
4+
5+
void loop() {
6+
7+
}

test/test_compile.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -695,3 +695,34 @@ def test_compile_sketch_with_multiple_main_files(run_command, data_dir):
695695
res = run_command(f"compile --clean -b {fqbn} {sketch_pde_file}")
696696
assert res.failed
697697
assert "Error during build: opening sketch: multiple main sketch files found" in res.stderr
698+
699+
700+
def test_compile_sketch_case_mismatch_fails(run_command, data_dir):
701+
# Init the environment explicitly
702+
assert run_command("update")
703+
704+
# Install core to compile
705+
assert run_command("core install arduino:[email protected]")
706+
707+
sketch_name = "CompileSketchCaseMismatch"
708+
sketch_path = Path(data_dir, sketch_name)
709+
fqbn = "arduino:avr:uno"
710+
711+
assert run_command(f"sketch new {sketch_path}")
712+
713+
# Rename main .ino file so casing is different from sketch name
714+
sketch_main_file = Path(sketch_path, f"{sketch_name}.ino").rename(sketch_path / f"{sketch_name.lower()}.ino")
715+
716+
# Verifies compilation fails when:
717+
# * Compiling with sketch path
718+
res = run_command(f"compile --clean -b {fqbn} {sketch_path}")
719+
assert res.failed
720+
assert "Error during build: opening sketch: no valid sketch found" in res.stderr
721+
# * Compiling with sketch main file
722+
res = run_command(f"compile --clean -b {fqbn} {sketch_main_file}")
723+
assert res.failed
724+
assert "Error during build: opening sketch: no valid sketch found" in res.stderr
725+
# * Compiling in sketch path
726+
res = run_command(f"compile --clean -b {fqbn}", custom_working_dir=sketch_path)
727+
assert res.failed
728+
assert "Error during build: opening sketch: no valid sketch found" in res.stderr

test/test_lib.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -668,3 +668,38 @@ def test_lib_examples_with_pde_file(run_command, data_dir):
668668
assert str(Path(data_dir, "libraries", "Encoder", "examples", "NoInterrupts")) in examples
669669
assert str(Path(data_dir, "libraries", "Encoder", "examples", "SpeedTest")) in examples
670670
assert str(Path(data_dir, "libraries", "Encoder", "examples", "TwoKnobs")) in examples
671+
672+
673+
def test_lib_examples_with_case_mismatch(run_command, data_dir):
674+
assert run_command("update")
675+
676+
assert run_command("lib install [email protected]")
677+
678+
res = run_command("lib examples WiFiManager --format json")
679+
assert res.ok
680+
data = json.loads(res.stdout)
681+
assert len(data) == 1
682+
examples = data[0]["examples"]
683+
684+
assert len(examples) == 14
685+
686+
examples_path = Path(data_dir, "libraries", "WiFiManager", "examples")
687+
# Verifies sketches with correct casing are listed
688+
assert str(examples_path / "Advanced") in examples
689+
assert str(examples_path / "AutoConnect" / "AutoConnectWithFeedbackLED") in examples
690+
assert str(examples_path / "AutoConnect" / "AutoConnectWithFSParameters") in examples
691+
assert str(examples_path / "AutoConnect" / "AutoConnectWithFSParametersAndCustomIP") in examples
692+
assert str(examples_path / "Basic") in examples
693+
assert str(examples_path / "DEV" / "OnDemandConfigPortal") in examples
694+
assert str(examples_path / "NonBlocking" / "AutoConnectNonBlocking") in examples
695+
assert str(examples_path / "NonBlocking" / "AutoConnectNonBlockingwParams") in examples
696+
assert str(examples_path / "Old_examples" / "AutoConnectWithFeedback") in examples
697+
assert str(examples_path / "Old_examples" / "AutoConnectWithReset") in examples
698+
assert str(examples_path / "Old_examples" / "AutoConnectWithStaticIP") in examples
699+
assert str(examples_path / "Old_examples" / "AutoConnectWithTimeout") in examples
700+
assert str(examples_path / "OnDemand" / "OnDemandConfigPortal") in examples
701+
assert str(examples_path / "ParamsChildClass") in examples
702+
703+
# Verifies sketches with wrong casing are not returned
704+
assert str(examples_path / "NonBlocking" / "OnDemandNonBlocking") not in examples
705+
assert str(examples_path / "OnDemand" / "OnDemandWebPortal") not in examples

test/test_sketch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,17 @@ def test_sketch_archive_with_multiple_main_files(run_command, copy_sketch, worki
846846
assert "Sketches with .pde extension are deprecated, please rename the following files to .ino" in res.stderr
847847
assert str(sketch_file.relative_to(sketch_dir)) in res.stderr
848848
assert "Error archiving: multiple main sketch files found" in res.stderr
849+
850+
851+
def test_sketch_archive_case_mismatch_fails(run_command, data_dir):
852+
sketch_name = "ArchiveSketchCaseMismatch"
853+
sketch_path = Path(data_dir, sketch_name)
854+
855+
assert run_command(f"sketch new {sketch_path}")
856+
857+
# Rename main .ino file so casing is different from sketch name
858+
Path(sketch_path, f"{sketch_name}.ino").rename(sketch_path / f"{sketch_name.lower()}.ino")
859+
860+
res = run_command(f'sketch archive "{sketch_path}"')
861+
assert res.failed
862+
assert "Error archiving: no valid sketch found" in res.stderr

test/test_upload.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,3 +337,48 @@ def test_upload_with_input_dir_containing_multiple_binaries(run_command, data_di
337337
assert (
338338
"Sketches with .pde extension are deprecated, please rename the following files to .ino:" not in res.stderr
339339
)
340+
341+
342+
def test_compile_and_upload_combo_sketch_with_mismatched_casing(run_command, data_dir, detected_boards, wait_for_board):
343+
assert run_command("update")
344+
345+
# Create a sketch
346+
sketch_name = "CompileUploadComboMismatchCasing"
347+
sketch_path = Path(data_dir, sketch_name)
348+
assert run_command(f"sketch new {sketch_path}")
349+
350+
# Rename main .ino file so casing is different from sketch name
351+
Path(sketch_path, f"{sketch_name}.ino").rename(sketch_path / f"{sketch_name.lower()}.ino")
352+
353+
for board in detected_boards:
354+
# Install core
355+
core = ":".join(board.fqbn.split(":")[:2])
356+
assert run_command(f"core install {core}")
357+
358+
# Try to compile
359+
res = run_command(f"compile --clean -b {board.fqbn} -u -p {board.address} {sketch_path}")
360+
assert res.failed
361+
assert "Error during build: opening sketch: no valid sketch found" in res.stderr
362+
363+
364+
def test_upload_sketch_with_mismatched_casing(run_command, data_dir, detected_boards, wait_for_board):
365+
assert run_command("update")
366+
367+
# Create a sketch
368+
sketch_name = "UploadMismatchCasing"
369+
sketch_path = Path(data_dir, sketch_name)
370+
assert run_command(f"sketch new {sketch_path}")
371+
372+
# Rename main .ino file so casing is different from sketch name
373+
Path(sketch_path, f"{sketch_name}.ino").rename(sketch_path / f"{sketch_name.lower()}.ino")
374+
375+
for board in detected_boards:
376+
# Install core
377+
core = ":".join(board.fqbn.split(":")[:2])
378+
assert run_command(f"core install {core}")
379+
380+
# Tries to upload given sketch, it has not been compiled but it fails even before
381+
# searching for binaries since the sketch is not valid
382+
res = run_command(f"upload -b {board.fqbn} -p {board.address} {sketch_path}")
383+
assert res.failed
384+
assert "Error during Upload: opening sketch: no valid sketch found" in res.stderr

0 commit comments

Comments
 (0)