View Javadoc
1   /*
2    *    Copyright 2011-2026 the original author or authors.
3    *
4    *    This program is free software; you can redistribute it and/or
5    *    modify it under the terms of the GNU General Public License
6    *    as published by the Free Software Foundation; either version 2
7    *    of the License, or (at your option) any later version.
8    *
9    *    You may obtain a copy of the License at
10   *
11   *       https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
12   *
13   *    This program is distributed in the hope that it will be useful,
14   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
15   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16   *    GNU General Public License for more details.
17   */
18  package com.hazendaz.maven.makeself;
19  
20  import java.io.File;
21  import java.lang.reflect.Field;
22  import java.lang.reflect.Method;
23  import java.nio.file.Files;
24  import java.nio.file.Path;
25  import java.util.Arrays;
26  import java.util.List;
27  
28  import org.apache.maven.plugin.MojoExecutionException;
29  import org.apache.maven.plugin.logging.Log;
30  import org.apache.maven.project.MavenProject;
31  import org.apache.maven.project.MavenProjectHelper;
32  import org.eclipse.aether.RepositorySystemSession;
33  import org.eclipse.aether.repository.LocalRepository;
34  import org.junit.jupiter.api.Assertions;
35  import org.junit.jupiter.api.Assumptions;
36  import org.junit.jupiter.api.Test;
37  import org.junit.jupiter.api.extension.ExtendWith;
38  import org.junit.jupiter.api.io.TempDir;
39  import org.mockito.Mock;
40  import org.mockito.Mockito;
41  import org.mockito.junit.jupiter.MockitoExtension;
42  
43  /**
44   * The Class MakeselfMojoTest.
45   */
46  @ExtendWith(MockitoExtension.class)
47  class MakeselfMojoTest {
48  
49      /** Temporary directory for test files. */
50      @TempDir
51      Path tempDir;
52  
53      /** Mock Maven log. */
54      @Mock
55      private Log log;
56  
57      /** Mock repository system session for portableGit tests. */
58      @Mock
59      private RepositorySystemSession repoSession;
60  
61      /**
62       * Sets a private field on the given object via reflection, walking up the class hierarchy as needed.
63       *
64       * @param obj
65       *            the object
66       * @param fieldName
67       *            the field name
68       * @param value
69       *            the value to set
70       *
71       * @throws Exception
72       *             if the field cannot be found or set
73       */
74      private static void setField(final Object obj, final String fieldName, final Object value) throws Exception {
75          final Field field = findField(obj.getClass(), fieldName);
76          field.setAccessible(true);
77          field.set(obj, value);
78      }
79  
80      /**
81       * Finds a declared field by name, walking the class hierarchy.
82       *
83       * @param clazz
84       *            the class to start from
85       * @param name
86       *            the field name
87       *
88       * @return the field
89       *
90       * @throws NoSuchFieldException
91       *             if no field with the given name is found
92       */
93      private static Field findField(final Class<?> clazz, final String name) throws NoSuchFieldException {
94          try {
95              return clazz.getDeclaredField(name);
96          } catch (final NoSuchFieldException e) {
97              if (clazz.getSuperclass() != null) {
98                  return findField(clazz.getSuperclass(), name);
99              }
100             throw e;
101         }
102     }
103 
104     /**
105      * Invokes the private {@code loadArgs} method on the given mojo via reflection.
106      *
107      * @param mojo
108      *            the mojo instance
109      *
110      * @return the list of command-line arguments built by loadArgs
111      *
112      * @throws Exception
113      *             if the method cannot be invoked
114      */
115     @SuppressWarnings("unchecked")
116     private static List<String> callLoadArgs(final MakeselfMojo mojo) throws Exception {
117         final Method method = MakeselfMojo.class.getDeclaredMethod("loadArgs");
118         method.setAccessible(true);
119         return (List<String>) method.invoke(mojo);
120     }
121 
122     /**
123      * Invokes the private {@code isTrue} method on the given mojo via reflection.
124      *
125      * @param mojo
126      *            the mojo instance
127      * @param value
128      *            the Boolean value to test
129      *
130      * @return the result of isTrue
131      *
132      * @throws Exception
133      *             if the method cannot be invoked
134      */
135     private static boolean callIsTrue(final MakeselfMojo mojo, final Boolean value) throws Exception {
136         final Method method = MakeselfMojo.class.getDeclaredMethod("isTrue", Boolean.class);
137         method.setAccessible(true);
138         return (boolean) method.invoke(mojo, value);
139     }
140 
141     /**
142      * Test skip execution logs message and returns without error.
143      *
144      * @throws Exception
145      *             the exception
146      */
147     @Test
148     void testSkipExecution() throws Exception {
149         final MakeselfMojo mojo = new MakeselfMojo();
150         mojo.setLog(log);
151         setField(mojo, "skip", true);
152 
153         mojo.execute();
154 
155         Mockito.verify(log).info("Makeself is skipped");
156     }
157 
158     /**
159      * Test missing archive dir throws MojoExecutionException.
160      *
161      * @throws Exception
162      *             the exception
163      */
164     @Test
165     void testMissingArchiveDir() throws Exception {
166         final MakeselfMojo mojo = new MakeselfMojo();
167         mojo.setLog(log);
168         setField(mojo, "buildTarget", tempDir.toString() + "/");
169         setField(mojo, "archiveDir", "does-not-exist");
170 
171         Assertions.assertThrows(MojoExecutionException.class, mojo::execute);
172     }
173 
174     /**
175      * Test inline script without script args throws MojoExecutionException.
176      *
177      * @throws Exception
178      *             the exception
179      */
180     @Test
181     void testInlineScriptWithoutScriptArgs() throws Exception {
182         final MakeselfMojo mojo = new MakeselfMojo();
183         mojo.setLog(log);
184         Files.createDirectory(tempDir.resolve("archive"));
185         setField(mojo, "inlineScript", true);
186         setField(mojo, "buildTarget", tempDir.toString() + "/");
187         setField(mojo, "archiveDir", "archive");
188         // scriptArgs remains null
189 
190         Assertions.assertThrows(MojoExecutionException.class, mojo::execute);
191     }
192 
193     /**
194      * Test startup script without leading dot-slash throws MojoExecutionException.
195      *
196      * @throws Exception
197      *             the exception
198      */
199     @Test
200     void testStartupScriptWithoutDotSlash() throws Exception {
201         final MakeselfMojo mojo = new MakeselfMojo();
202         mojo.setLog(log);
203         Files.createDirectory(tempDir.resolve("archive"));
204         setField(mojo, "buildTarget", tempDir.toString() + "/");
205         setField(mojo, "archiveDir", "archive");
206         setField(mojo, "startupScript", "script.sh");
207 
208         Assertions.assertThrows(MojoExecutionException.class, mojo::execute);
209     }
210 
211     /**
212      * Test startup script file missing throws MojoExecutionException.
213      *
214      * @throws Exception
215      *             the exception
216      */
217     @Test
218     void testStartupScriptFileMissing() throws Exception {
219         final MakeselfMojo mojo = new MakeselfMojo();
220         mojo.setLog(log);
221         Files.createDirectory(tempDir.resolve("archive"));
222         setField(mojo, "buildTarget", tempDir.toString() + "/");
223         setField(mojo, "archiveDir", "archive");
224         setField(mojo, "startupScript", "./nonexistent.sh");
225 
226         Assertions.assertThrows(MojoExecutionException.class, mojo::execute);
227     }
228 
229     /**
230      * Test load args returns empty list when all optional parameters are null.
231      *
232      * @throws Exception
233      *             the exception
234      */
235     @Test
236     void testLoadArgsEmpty() throws Exception {
237         final MakeselfMojo mojo = new MakeselfMojo();
238         mojo.setLog(log);
239 
240         final List<String> args = callLoadArgs(mojo);
241 
242         Assertions.assertTrue(args.isEmpty());
243     }
244 
245     /**
246      * Test load args includes the correct CLI flag for every boolean parameter when set to true.
247      *
248      * @throws Exception
249      *             the exception
250      */
251     @Test
252     void testLoadArgsBooleanFlags() throws Exception {
253         final MakeselfMojo mojo = new MakeselfMojo();
254         mojo.setLog(log);
255         setField(mojo, "tarQuietly", Boolean.TRUE);
256         setField(mojo, "quiet", Boolean.TRUE);
257         setField(mojo, "gzip", Boolean.TRUE);
258         setField(mojo, "bzip2", Boolean.TRUE);
259         setField(mojo, "bzip3", Boolean.TRUE);
260         setField(mojo, "pbzip2", Boolean.TRUE);
261         setField(mojo, "xz", Boolean.TRUE);
262         setField(mojo, "lzo", Boolean.TRUE);
263         setField(mojo, "lz4", Boolean.TRUE);
264         setField(mojo, "zstd", Boolean.TRUE);
265         setField(mojo, "pigz", Boolean.TRUE);
266         setField(mojo, "base64", Boolean.TRUE);
267         setField(mojo, "gpgEncrypt", Boolean.TRUE);
268         setField(mojo, "gpgAsymmetricEncryptSign", Boolean.TRUE);
269         setField(mojo, "sslEncrypt", Boolean.TRUE);
270         setField(mojo, "sslNoMd", Boolean.TRUE);
271         setField(mojo, "compress", Boolean.TRUE);
272         setField(mojo, "nochown", Boolean.TRUE);
273         setField(mojo, "chown", Boolean.TRUE);
274         setField(mojo, "nocomp", Boolean.TRUE);
275         setField(mojo, "notemp", Boolean.TRUE);
276         setField(mojo, "needroot", Boolean.TRUE);
277         setField(mojo, "current", Boolean.TRUE);
278         setField(mojo, "follow", Boolean.TRUE);
279         setField(mojo, "noprogress", Boolean.TRUE);
280         setField(mojo, "append", Boolean.TRUE);
281         setField(mojo, "copy", Boolean.TRUE);
282         setField(mojo, "nox11", Boolean.TRUE);
283         setField(mojo, "nowait", Boolean.TRUE);
284         setField(mojo, "nomd5", Boolean.TRUE);
285         setField(mojo, "nocrc", Boolean.TRUE);
286         setField(mojo, "sha256", Boolean.TRUE);
287         setField(mojo, "keepUmask", Boolean.TRUE);
288         setField(mojo, "exportConf", Boolean.TRUE);
289         setField(mojo, "nooverwrite", Boolean.TRUE);
290 
291         final List<String> args = callLoadArgs(mojo);
292 
293         Assertions.assertTrue(args.containsAll(Arrays.asList("--tar-quietly", "--quiet", "--gzip", "--bzip2", "--bzip3",
294                 "--pbzip2", "--xz", "--lzo", "--lz4", "--zstd", "--pigz", "--base64", "--gpg-encrypt",
295                 "--gpg-asymmetric-encrypt-sign", "--ssl-encrypt", "--ssl-no-md", "--compress", "--nochown", "--chown",
296                 "--nocomp", "--notemp", "--needroot", "--current", "--follow", "--noprogress", "--append", "--copy",
297                 "--nox11", "--nowait", "--nomd5", "--nocrc", "--sha256", "--keep-umask", "--export-conf",
298                 "--nooverwrite")));
299     }
300 
301     /**
302      * Test load args includes the correct flag-value pairs for all string and integer parameters.
303      *
304      * @throws Exception
305      *             the exception
306      */
307     @Test
308     void testLoadArgsStringAndIntOptions() throws Exception {
309         final MakeselfMojo mojo = new MakeselfMojo();
310         mojo.setLog(log);
311         setField(mojo, "sslPasswd", "mypassword");
312         setField(mojo, "sslPassSrc", "env:MY_PASS");
313         setField(mojo, "complevel", 6);
314         setField(mojo, "compExtra", "--extra-opt");
315         setField(mojo, "threads", 4);
316         setField(mojo, "headerFile", "my-header.sh");
317         setField(mojo, "preextractScript", "preextract.sh");
318         setField(mojo, "cleanupScript", "cleanup.sh");
319         setField(mojo, "lsmFile", "my-package.lsm");
320         setField(mojo, "gpgExtraOpt", "--gpg-opt");
321         setField(mojo, "tarFormatOpt", "posix");
322         setField(mojo, "tarExtraOpt", "--exclude=.git");
323         setField(mojo, "untarExtraOpt", "--strip=1");
324         setField(mojo, "signPassphrase", "signingkey");
325         setField(mojo, "extractTargetDir", "/opt/myapp");
326         setField(mojo, "packagingDate", "2026-01-01");
327         setField(mojo, "licenseFile", "LICENSE");
328         setField(mojo, "helpHeaderFile", "help-header.txt");
329 
330         final List<String> args = callLoadArgs(mojo);
331 
332         Assertions.assertAll(() -> Assertions.assertTrue(args.contains("--ssl-passwd") && args.contains("mypassword")),
333                 () -> Assertions.assertTrue(args.contains("--ssl-pass-src") && args.contains("env:MY_PASS")),
334                 () -> Assertions.assertTrue(args.contains("--complevel") && args.contains("6")),
335                 () -> Assertions.assertTrue(args.contains("--comp-extra") && args.contains("--extra-opt")),
336                 () -> Assertions.assertTrue(args.contains("--threads") && args.contains("4")),
337                 () -> Assertions.assertTrue(args.contains("--header") && args.contains("my-header.sh")),
338                 () -> Assertions.assertTrue(args.contains("--preextract") && args.contains("preextract.sh")),
339                 () -> Assertions.assertTrue(args.contains("--cleanup") && args.contains("cleanup.sh")),
340                 () -> Assertions.assertTrue(args.contains("--lsm") && args.contains("my-package.lsm")),
341                 () -> Assertions.assertTrue(args.contains("--gpg-extra") && args.contains("--gpg-opt")),
342                 () -> Assertions.assertTrue(args.contains("--tar-format") && args.contains("posix")),
343                 () -> Assertions.assertTrue(args.contains("--tar-extra") && args.contains("--exclude=.git")),
344                 () -> Assertions.assertTrue(args.contains("--untar-extra") && args.contains("--strip=1")),
345                 () -> Assertions.assertTrue(args.contains("--sign") && args.contains("signingkey")),
346                 () -> Assertions.assertTrue(args.contains("--target") && args.contains("/opt/myapp")),
347                 () -> Assertions.assertTrue(args.contains("--packaging-date") && args.contains("2026-01-01")),
348                 () -> Assertions.assertTrue(args.contains("--license") && args.contains("LICENSE")),
349                 () -> Assertions.assertTrue(args.contains("--help-header") && args.contains("help-header.txt")));
350     }
351 
352     /**
353      * Test is true returns false for null, true for Boolean.TRUE, and false for Boolean.FALSE.
354      *
355      * @throws Exception
356      *             the exception
357      */
358     @Test
359     void testIsTrue() throws Exception {
360         final MakeselfMojo mojo = new MakeselfMojo();
361 
362         Assertions.assertAll(() -> Assertions.assertFalse(callIsTrue(mojo, null)),
363                 () -> Assertions.assertTrue(callIsTrue(mojo, Boolean.TRUE)),
364                 () -> Assertions.assertFalse(callIsTrue(mojo, Boolean.FALSE)));
365     }
366 
367     /**
368      * Test extractMakeself creates the makeself.sh and makeself-header.sh files in the configured temp directory.
369      *
370      * @throws Exception
371      *             the exception
372      */
373     @Test
374     void testExtractMakeself() throws Exception {
375         final MakeselfMojo mojo = new MakeselfMojo();
376         mojo.setLog(log);
377         setField(mojo, "makeselfTempDirectory", tempDir.toFile());
378 
379         final Method method = MakeselfMojo.class.getDeclaredMethod("extractMakeself");
380         method.setAccessible(true);
381         method.invoke(mojo);
382 
383         Assertions.assertTrue(Files.exists(tempDir.resolve("makeself.sh")),
384                 "makeself.sh should be extracted to temp directory");
385         Assertions.assertTrue(Files.exists(tempDir.resolve("makeself-header.sh")),
386                 "makeself-header.sh should be extracted to temp directory");
387     }
388 
389     /**
390      * Test extractMakeself is idempotent: a second call when files already exist does not overwrite them.
391      *
392      * @throws Exception
393      *             the exception
394      */
395     @Test
396     void testExtractMakeselfIdempotent() throws Exception {
397         final MakeselfMojo mojo = new MakeselfMojo();
398         mojo.setLog(log);
399         setField(mojo, "makeselfTempDirectory", tempDir.toFile());
400 
401         final Method method = MakeselfMojo.class.getDeclaredMethod("extractMakeself");
402         method.setAccessible(true);
403 
404         // First call – extracts files
405         method.invoke(mojo);
406         final long modifiedFirst = tempDir.resolve("makeself.sh").toFile().lastModified();
407 
408         // Second call – files already exist, should be skipped
409         method.invoke(mojo);
410         final long modifiedSecond = tempDir.resolve("makeself.sh").toFile().lastModified();
411 
412         Assertions.assertEquals(modifiedFirst, modifiedSecond, "makeself.sh should not be overwritten on second call");
413     }
414 
415     /**
416      * Test setFilePermissions makes the file executable and logs the outcome.
417      *
418      * @throws Exception
419      *             the exception
420      */
421     @Test
422     void testSetFilePermissions() throws Exception {
423         final MakeselfMojo mojo = new MakeselfMojo();
424         mojo.setLog(log);
425 
426         final File tempFile = Files.createTempFile(tempDir, "test", ".sh").toFile();
427 
428         final Method method = MakeselfMojo.class.getDeclaredMethod("setFilePermissions", File.class);
429         method.setAccessible(true);
430         method.invoke(mojo, tempFile);
431 
432         Assertions.assertTrue(tempFile.canExecute(), "File should be executable after setFilePermissions");
433     }
434 
435     /**
436      * Test setPosixFilePermissions applies permissions to a regular file on a POSIX-capable filesystem. On systems
437      * where POSIX attributes are unsupported the call is expected to be a silent no-op.
438      *
439      * @throws Exception
440      *             the exception
441      */
442     @Test
443     void testSetPosixFilePermissions() throws Exception {
444         final MakeselfMojo mojo = new MakeselfMojo();
445         mojo.setLog(log);
446 
447         final Path tempFile = Files.createTempFile(tempDir, "test", ".sh");
448 
449         final Method method = MakeselfMojo.class.getDeclaredMethod("setPosixFilePermissions", Path.class);
450         method.setAccessible(true);
451         // Should not throw, either sets permissions or logs the unsupported-operation debug message
452         method.invoke(mojo, tempFile);
453     }
454 
455     /**
456      * Helper that creates the standard directory/file layout required for a full execute() run and returns the
457      * configured mojo.
458      *
459      * @param archiveDirName
460      *            name of the archive subdirectory to create
461      * @param startupScriptName
462      *            name of the startup script to create inside archiveDirName (without leading ./)
463      *
464      * @return a fully configured MakeselfMojo ready to run
465      *
466      * @throws Exception
467      *             if setup fails
468      */
469     private MakeselfMojo buildFullFlowMojo(final String archiveDirName, final String startupScriptName)
470             throws Exception {
471         final Path archivePath = Files.createDirectories(tempDir.resolve(archiveDirName));
472         Files.createFile(archivePath.resolve(startupScriptName));
473 
474         final File makeselfTempDir = Files.createDirectories(tempDir.resolve("makeself-tmp")).toFile();
475         final MavenProjectHelper projectHelper = Mockito.mock(MavenProjectHelper.class);
476         final MavenProject project = new MavenProject();
477 
478         final MakeselfMojo mojo = new MakeselfMojo();
479         mojo.setLog(log);
480         setField(mojo, "buildTarget", tempDir.toString() + "/");
481         setField(mojo, "archiveDir", archiveDirName);
482         setField(mojo, "startupScript", "./" + startupScriptName);
483         setField(mojo, "fileName", "output.sh");
484         setField(mojo, "label", "Test Archive");
485         setField(mojo, "makeselfTempDirectory", makeselfTempDir);
486         setField(mojo, "projectHelper", projectHelper);
487         setField(mojo, "project", project);
488         return mojo;
489     }
490 
491     /**
492      * Test the full execute() flow on non-Windows: validates, extracts makeself stubs, runs bash commands, and attaches
493      * the produced artifact.
494      *
495      * @throws Exception
496      *             the exception
497      */
498     @Test
499     void testExecuteFullFlowOnLinux() throws Exception {
500         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test only applicable on non-Windows");
501 
502         final MakeselfMojo mojo = buildFullFlowMojo("makeself", "makeself.sh");
503 
504         mojo.execute();
505 
506         Mockito.verify(log).info("Running makeself build");
507     }
508 
509     /**
510      * Test execute() returns early when the version flag is set, without running the makeself build.
511      *
512      * @throws Exception
513      *             the exception
514      */
515     @Test
516     void testExecuteWithVersionFlag() throws Exception {
517         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test only applicable on non-Windows");
518 
519         final MakeselfMojo mojo = buildFullFlowMojo("makeself", "makeself.sh");
520         setField(mojo, "version", Boolean.TRUE);
521 
522         mojo.execute();
523 
524         Mockito.verify(log, Mockito.never()).info("Running makeself build");
525     }
526 
527     /**
528      * Test execute() with the help flag set runs the makeself --help command and returns without building.
529      *
530      * @throws Exception
531      *             the exception
532      */
533     @Test
534     void testExecuteWithHelpFlag() throws Exception {
535         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test only applicable on non-Windows");
536 
537         final MakeselfMojo mojo = buildFullFlowMojo("makeself", "makeself.sh");
538         setField(mojo, "help", Boolean.TRUE);
539 
540         mojo.execute();
541 
542         Mockito.verify(log, Mockito.never()).info("Running makeself build");
543     }
544 
545     /**
546      * Test execute() with autoRun=true causes the resulting script to be invoked automatically.
547      *
548      * @throws Exception
549      *             the exception
550      */
551     @Test
552     void testExecuteWithAutoRun() throws Exception {
553         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test only applicable on non-Windows");
554 
555         final MakeselfMojo mojo = buildFullFlowMojo("makeself", "makeself.sh");
556         setField(mojo, "autoRun", true);
557 
558         mojo.execute();
559 
560         Mockito.verify(log).info("Auto-run created shell (this may take a few minutes)");
561     }
562 
563     /**
564      * Test execute() with inlineScript=true and scriptArgs set skips the startup script file check and runs the build.
565      *
566      * @throws Exception
567      *             the exception
568      */
569     @Test
570     void testExecuteWithInlineScriptAndScriptArgs() throws Exception {
571         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test only applicable on non-Windows");
572 
573         final Path archivePath = Files.createDirectories(tempDir.resolve("myarchive"));
574         Files.createFile(archivePath.resolve("dummy.txt"));
575 
576         final File makeselfTempDir = Files.createDirectories(tempDir.resolve("makeself-tmp")).toFile();
577         final MavenProjectHelper projectHelper = Mockito.mock(MavenProjectHelper.class);
578         final MavenProject project = new MavenProject();
579 
580         final MakeselfMojo mojo = new MakeselfMojo();
581         mojo.setLog(log);
582         setField(mojo, "buildTarget", tempDir.toString() + "/");
583         setField(mojo, "archiveDir", "myarchive");
584         setField(mojo, "startupScript", "echo");
585         setField(mojo, "inlineScript", true);
586         setField(mojo, "scriptArgs", Arrays.asList("hello"));
587         setField(mojo, "fileName", "output.sh");
588         setField(mojo, "label", "Test Archive");
589         setField(mojo, "makeselfTempDirectory", makeselfTempDir);
590         setField(mojo, "projectHelper", projectHelper);
591         setField(mojo, "project", project);
592 
593         mojo.execute();
594 
595         Mockito.verify(log).info("Running makeself build");
596     }
597 
598     /**
599      * Test loadArgs does not include flags for Boolean.FALSE parameters.
600      *
601      * @throws Exception
602      *             the exception
603      */
604     @Test
605     void testLoadArgsWithFalseFlags() throws Exception {
606         final MakeselfMojo mojo = new MakeselfMojo();
607         mojo.setLog(log);
608         setField(mojo, "gzip", Boolean.FALSE);
609         setField(mojo, "bzip2", Boolean.FALSE);
610         setField(mojo, "xz", Boolean.FALSE);
611         setField(mojo, "nocomp", Boolean.FALSE);
612         setField(mojo, "nomd5", Boolean.FALSE);
613 
614         final List<String> args = callLoadArgs(mojo);
615 
616         Assertions.assertFalse(args.contains("--gzip"), "--gzip should not be present when gzip=false");
617         Assertions.assertFalse(args.contains("--bzip2"), "--bzip2 should not be present when bzip2=false");
618         Assertions.assertFalse(args.contains("--xz"), "--xz should not be present when xz=false");
619         Assertions.assertFalse(args.contains("--nocomp"), "--nocomp should not be present when nocomp=false");
620         Assertions.assertFalse(args.contains("--nomd5"), "--nomd5 should not be present when nomd5=false");
621     }
622 
623     /**
624      * Test execute() on a simulated Windows platform with an existing git path. The Windows branch in the outer
625      * execute() is taken (gitPath is not empty and the path exists), then the private execute() fails with IOException
626      * because the fake bash path does not exist. Verifies the IOException is caught and logged, not re-thrown.
627      *
628      * @throws Exception
629      *             the exception
630      */
631     @Test
632     void testExecuteWindowsSimulatedExistingGitPath() throws Exception {
633         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Windows-simulation test is only run on non-Windows");
634 
635         final MakeselfMojo mojo = new MakeselfMojo() {
636             @Override
637             protected boolean isWindows() {
638                 return true;
639             }
640         };
641         mojo.setLog(log);
642 
643         // Minimal mojo setup required for validate() to pass
644         final Path archivePath = Files.createDirectories(tempDir.resolve("archive-w"));
645         Files.createFile(archivePath.resolve("startup.sh"));
646         final File makeselfTempDir = Files.createDirectories(tempDir.resolve("makeself-tmp-w")).toFile();
647         final MavenProjectHelper projectHelper = Mockito.mock(MavenProjectHelper.class);
648         final MavenProject project = new MavenProject();
649 
650         setField(mojo, "buildTarget", tempDir.toString() + "/");
651         setField(mojo, "archiveDir", "archive-w");
652         setField(mojo, "startupScript", "./startup.sh");
653         setField(mojo, "fileName", "output-w.sh");
654         setField(mojo, "label", "Test");
655         setField(mojo, "makeselfTempDirectory", makeselfTempDir);
656         setField(mojo, "projectHelper", projectHelper);
657         setField(mojo, "project", project);
658 
659         // Set gitPath to a directory that exists (tempDir) so the existing-path branch is taken
660         setField(mojo, "gitPath", tempDir.toString());
661 
662         // execute() catches IOException internally; should not throw
663         mojo.execute();
664 
665         // The IOException from starting a non-existent bash is caught and logged
666         Mockito.verify(log).error(Mockito.eq(""), Mockito.any(Exception.class));
667     }
668 
669     /**
670      * Test execute() on a simulated Windows platform when checkGitSetup() sets gitPath to the system bash directory.
671      * This exercises the else branch of the gitPath check (gitPath is empty), the full bash command flow, and verifies
672      * that the {@code --list} command is skipped on Windows.
673      *
674      * @throws Exception
675      *             the exception
676      */
677     @Test
678     void testExecuteWindowsSimulatedCheckGitSetup() throws Exception {
679         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Windows-simulation test is only run on non-Windows");
680 
681         final MakeselfMojo mojo = new MakeselfMojo() {
682             @Override
683             protected boolean isWindows() {
684                 return true;
685             }
686 
687             @Override
688             public void checkGitSetup() {
689                 // Simulate checkGitSetup: leave gitPath empty so bash resolves from PATH
690             }
691         };
692         mojo.setLog(log);
693 
694         final MakeselfMojo configured = mojo;
695         final MavenProjectHelper projectHelper = Mockito.mock(MavenProjectHelper.class);
696         final MavenProject project = new MavenProject();
697 
698         final Path archivePath = Files.createDirectories(tempDir.resolve("archive-wcs"));
699         Files.createFile(archivePath.resolve("startup.sh"));
700         final File makeselfTempDir = Files.createDirectories(tempDir.resolve("makeself-tmp-wcs")).toFile();
701 
702         setField(configured, "buildTarget", tempDir.toString() + "/");
703         setField(configured, "archiveDir", "archive-wcs");
704         setField(configured, "startupScript", "./startup.sh");
705         setField(configured, "fileName", "output-wcs.sh");
706         setField(configured, "label", "Test");
707         setField(configured, "makeselfTempDirectory", makeselfTempDir);
708         setField(configured, "projectHelper", projectHelper);
709         setField(configured, "project", project);
710 
711         // execute() should run the full flow without throwing
712         mojo.execute();
713 
714         Mockito.verify(log).info("Running makeself build");
715     }
716 
717     /**
718      * Test the private execute() method with portableGit set, simulating Windows. Exercises the portableGit != null
719      * branch in the Windows environment-variable setup inside execute(List, boolean).
720      *
721      * @throws Exception
722      *             the exception
723      */
724     @Test
725     void testExecutePrivateWindowsPortableGitNotNull() throws Exception {
726         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Windows-simulation test is only run on non-Windows");
727 
728         final MakeselfMojo mojo = new MakeselfMojo() {
729             @Override
730             protected boolean isWindows() {
731                 return true;
732             }
733         };
734         mojo.setLog(log);
735 
736         final PortableGit portableGit = new PortableGit(log);
737         setField(mojo, "portableGit", portableGit);
738 
739         final LocalRepository localRepository = new LocalRepository(tempDir.toFile());
740         Mockito.when(repoSession.getLocalRepository()).thenReturn(localRepository);
741         setField(mojo, "repoSession", repoSession);
742 
743         // Invoke the private execute(List, boolean) method directly
744         final Method executeMethod = MakeselfMojo.class.getDeclaredMethod("execute", List.class, boolean.class);
745         executeMethod.setAccessible(true);
746         executeMethod.invoke(mojo, Arrays.asList("bash", "--version"), false);
747 
748         // No exception means the portableGit != null PATH manipulation ran without error
749         Mockito.verify(log, Mockito.atLeastOnce()).debug(Mockito.anyString());
750     }
751 
752     /**
753      * Test setFilePermissions logs an error when setExecutable returns false (file cannot be made executable).
754      *
755      * @throws Exception
756      *             the exception
757      */
758     @Test
759     void testSetFilePermissionsFailure() throws Exception {
760         final MakeselfMojo mojo = new MakeselfMojo();
761         mojo.setLog(log);
762 
763         final File mockFile = Mockito.mock(File.class);
764         Mockito.when(mockFile.setExecutable(true, true)).thenReturn(false);
765         Mockito.when(mockFile.getName()).thenReturn("test-failure.sh");
766 
767         final Method method = MakeselfMojo.class.getDeclaredMethod("setFilePermissions", File.class);
768         method.setAccessible(true);
769         method.invoke(mojo, mockFile);
770 
771         Mockito.verify(log).error(Mockito.contains("Unable to set executable:"));
772     }
773 
774     /**
775      * Test setPosixFilePermissions logs an error when the path does not exist (IOException from
776      * Files.setPosixFilePermissions).
777      *
778      * @throws Exception
779      *             the exception
780      */
781     @Test
782     void testSetPosixFilePermissionsIOException() throws Exception {
783         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "POSIX permissions only apply on non-Windows");
784 
785         final MakeselfMojo mojo = new MakeselfMojo();
786         mojo.setLog(log);
787 
788         // A path that does not exist causes NoSuchFileException (subtype of IOException)
789         final Path nonExistent = tempDir.resolve("nonexistent-posix.sh");
790 
791         final Method method = MakeselfMojo.class.getDeclaredMethod("setPosixFilePermissions", Path.class);
792         method.setAccessible(true);
793         method.invoke(mojo, nonExistent);
794 
795         Mockito.verify(log).error(Mockito.eq("Failed attempted Posix permissions"), Mockito.any(Exception.class));
796     }
797 
798     /**
799      * Test extractMakeself creates its working directory when it does not yet exist (covers the mkdirs path in the
800      * condition guard).
801      *
802      * @throws Exception
803      *             the exception
804      */
805     @Test
806     void testExtractMakeselfCreatesNewDirectory() throws Exception {
807         final MakeselfMojo mojo = new MakeselfMojo();
808         mojo.setLog(log);
809 
810         // Use a subdirectory that does not exist yet so mkdirs() is called
811         final File newDir = tempDir.resolve("brand-new-makeself-dir").toFile();
812         Assertions.assertFalse(newDir.exists(), "Pre-condition: directory should not exist before the test");
813         setField(mojo, "makeselfTempDirectory", newDir);
814 
815         final Method method = MakeselfMojo.class.getDeclaredMethod("extractMakeself");
816         method.setAccessible(true);
817         method.invoke(mojo);
818 
819         Assertions.assertTrue(Files.exists(newDir.toPath().resolve("makeself.sh")),
820                 "makeself.sh should be extracted into the newly created directory");
821     }
822 
823     /**
824      * Test extractMakeself logs an error and returns early when the working directory cannot be created.
825      *
826      * @throws Exception
827      *             the exception
828      */
829     @Test
830     void testExtractMakeselfMkdirsFails() throws Exception {
831         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Test uses /dev/null which is only available on Unix");
832 
833         final MakeselfMojo mojo = new MakeselfMojo();
834         mojo.setLog(log);
835 
836         // /dev/null is a character device on Unix; mkdirs() on a path inside it always fails
837         final Path impossibleDir = Path.of("/dev/null/impossible-makeself-dir");
838         setField(mojo, "makeselfTempDirectory", impossibleDir.toFile());
839 
840         final Method method = MakeselfMojo.class.getDeclaredMethod("extractMakeself");
841         method.setAccessible(true);
842         method.invoke(mojo);
843 
844         Mockito.verify(log).error(Mockito.contains("Unable to make directory"));
845     }
846 
847     /**
848      * Test the private execute() method logs an error when the process exits with a non-zero status.
849      *
850      * @throws Exception
851      *             the exception
852      */
853     @Test
854     void testExecutePrivateFailStatus() throws Exception {
855         Assumptions.assumeFalse(AbstractGitMojo.WINDOWS, "Uses bash which is only available on Unix");
856 
857         final MakeselfMojo mojo = new MakeselfMojo();
858         mojo.setLog(log);
859 
860         final Method executeMethod = MakeselfMojo.class.getDeclaredMethod("execute", List.class, boolean.class);
861         executeMethod.setAccessible(true);
862         executeMethod.invoke(mojo, Arrays.asList("bash", "-c", "exit 1"), false);
863 
864         Mockito.verify(log).error(Mockito.contains("makeself failed with error status:"));
865     }
866 
867 }