• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 17731048468

15 Sep 2025 11:10AM UTC coverage: 68.638% (-0.7%) from 69.386%
17731048468

Pull #1455

github

web-flow
Merge 9fb74f613 into 443c2d907
Pull Request #1455: #1139 fix open ide NPE

3401 of 5421 branches covered (62.74%)

Branch coverage included in aggregate %.

8879 of 12470 relevant lines covered (71.2%)

3.12 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.11
cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java
1
package com.devonfw.tools.ide.io;
2

3
import java.io.OutputStream;
4
import java.io.Reader;
5
import java.nio.file.Path;
6
import java.nio.file.StandardCopyOption;
7
import java.nio.file.attribute.PosixFilePermission;
8
import java.time.Duration;
9
import java.util.List;
10
import java.util.Properties;
11
import java.util.Set;
12
import java.util.function.Consumer;
13
import java.util.function.Function;
14
import java.util.function.Predicate;
15

16
import com.devonfw.tools.ide.io.ini.IniFile;
17
import com.devonfw.tools.ide.io.ini.IniFileImpl;
18

19
/**
20
 * Interface that gives access to various operations on files.
21
 */
22
public interface FileAccess {
23

24
  /** {@link PosixFilePermission}s for "rwxr-xr-x" or 0755. */
25
  Set<PosixFilePermission> RWX_RX_RX = Set.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE,
10✔
26
      PosixFilePermission.GROUP_READ, PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OTHERS_READ, PosixFilePermission.OTHERS_EXECUTE);
27

28
  /**
29
   * Downloads a file from an arbitrary location.
30
   *
31
   * @param url the location of the binary file to download. May also be a local or remote path to copy from.
32
   * @param targetFile the {@link Path} to the target file to download to. Should not already exist. Missing parent directories will be created
33
   *     automatically.
34
   */
35
  void download(String url, Path targetFile);
36

37
  /**
38
   * Creates the entire {@link Path} as directories if not already existing.
39
   *
40
   * @param directory the {@link Path} to {@link java.nio.file.Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...) create}.
41
   */
42
  void mkdirs(Path directory);
43

44
  /**
45
   * @param file the {@link Path} to check.
46
   * @return {@code true} if the given {@code file} points to an existing file, {@code false} otherwise (the given {@link Path} does not exist or is a
47
   *     directory).
48
   */
49
  boolean isFile(Path file);
50

51
  /**
52
   * @param folder the {@link Path} to check.
53
   * @return {@code true} if the given {@code folder} points to an existing directory, {@code false} otherwise (a warning is logged in this case).
54
   */
55
  boolean isExpectedFolder(Path folder);
56

57
  /**
58
   * @param file the {@link Path} to compute the checksum of.
59
   * @param hashAlgorithm the hash algorithm (e.g. SHA-266).
60
   * @return the computed hash checksum as hex {@link String}.
61
   */
62
  String checksum(Path file, String hashAlgorithm);
63

64
  /**
65
   * Moves the given {@link Path} to the backup.
66
   *
67
   * @param fileOrFolder the {@link Path} to move to the backup (soft-deletion).
68
   * @return the {@link Path} in the backup where the given {@link Path} was moved to.
69
   */
70
  Path backup(Path fileOrFolder);
71

72
  /**
73
   * @param source the source {@link Path file or folder} to move.
74
   * @param targetDir the {@link Path} with the directory to move {@code source} into.
75
   * @param copyOptions the {@link java.nio.file.CopyOption} which specify how the move should be done
76
   */
77
  void move(Path source, Path targetDir, StandardCopyOption... copyOptions);
78

79
  /**
80
   * Creates a relative symbolic link. If the given {@code targetLink} already exists and is a symbolic link or a Windows junction, it will be replaced. In case
81
   * of missing privileges, Windows Junctions may be used as fallback, which must point to absolute paths. Therefore, the created link will be absolute instead
82
   * of relative.
83
   *
84
   * @param source the source {@link Path} to link to, may be relative or absolute.
85
   * @param targetLink the {@link Path} where the symbolic link shall be created pointing to {@code source}.
86
   */
87
  default void symlink(Path source, Path targetLink) {
88

89
    symlink(source, targetLink, true);
5✔
90
  }
1✔
91

92
  /**
93
   * Creates a {@link PathLinkType#SYMBOLIC_LINK symbolic link}. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will
94
   * be replaced. In case of missing privileges, Windows mklink may be used as fallback, which must point to absolute paths. In such case the {@code relative}
95
   * flag will be ignored.
96
   *
97
   * @param target the target {@link Path} to link to, may be relative or absolute.
98
   * @param link the {@link Path} where the symbolic link shall be created pointing to {@code target}.
99
   * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute.
100
   */
101
  default void symlink(Path target, Path link, boolean relative) {
102

103
    link(target, link, relative, PathLinkType.SYMBOLIC_LINK);
6✔
104
  }
1✔
105

106
  /**
107
   * Creates a {@link PathLinkType#HARD_LINK hard link}. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will be
108
   * replaced. In case of missing privileges, Windows mklink may be used as fallback.
109
   *
110
   * @param target the target {@link Path} to link to, may be relative or absolute.
111
   * @param link the {@link Path} where the symbolic link shall be created pointing to {@code target}.
112
   */
113
  default void hardlink(Path target, Path link) {
114

115
    link(target, link, false, PathLinkType.HARD_LINK);
×
116
  }
×
117

118
  /**
119
   * Creates a link. If the given {@code link} already exists and is a symbolic link or a Windows junction, it will be replaced. In case of missing privileges,
120
   * Windows mklink may be used as fallback, which must point to absolute paths. In such case the {@code relative} flag will be ignored.
121
   *
122
   * @param target the target {@link Path} to link to, may be relative or absolute.
123
   * @param link the {@link Path} where the symbolic link shall be created pointing to {@code target}.
124
   * @param relative - {@code true} if the symbolic link shall be relative, {@code false} if it shall be absolute.
125
   * @param type the {@link PathLinkType}.
126
   */
127
  void link(Path target, Path link, boolean relative, PathLinkType type);
128

129
  /**
130
   * @param link the {@link PathLink} to {@link #link(Path, Path, boolean, PathLinkType) create}.
131
   */
132
  default void link(PathLink link) {
133
    link(link.target(), link.link(), true, link.type());
×
134
  }
×
135

136
  /**
137
   * @param source the source {@link Path file or folder} to copy.
138
   * @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details. Will always ensure that in the end
139
   *     you will find the same content of {@code source} in {@code target}.
140
   */
141
  default void copy(Path source, Path target) {
142

143
    copy(source, target, FileCopyMode.COPY_TREE_FAIL_IF_EXISTS);
5✔
144
  }
1✔
145

146
  /**
147
   * @param source the source {@link Path file or folder} to copy.
148
   * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
149
   *     and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
150
   *     {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
151
   *     {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
152
   *     the same content of {@code source} in {@code target}.
153
   * @param mode the {@link FileCopyMode}.
154
   */
155
  default void copy(Path source, Path target, FileCopyMode mode) {
156

157
    copy(source, target, mode, PathCopyListener.NONE);
6✔
158
  }
1✔
159

160
  /**
161
   * @param source the source {@link Path file or folder} to copy.
162
   * @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not take the filename of {@code source}
163
   *     and copy that to {@code target} in case that is an existing folder. Instead it will always be simple and stupid and just copy from {@code source} to
164
   *     {@code target}. Therefore the result is always clear and easy to predict and understand. Also you can easily rename a file to copy. While
165
   *     {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always ensure that in the end you will find
166
   *     the same content of {@code source} in {@code target}.
167
   * @param mode the {@link FileCopyMode}.
168
   * @param listener the {@link PathCopyListener} that will be called for each copied {@link Path}.
169
   */
170
  void copy(Path source, Path target, FileCopyMode mode, PathCopyListener listener);
171

172
  /**
173
   * @param archiveFile the {@link Path} to the file to extract.
174
   * @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile} to.
175
   */
176
  default void extract(Path archiveFile, Path targetDir) {
177

178
    extract(archiveFile, targetDir, null);
5✔
179
  }
1✔
180

181
  /**
182
   * @param archiveFile the {@link Path} to the archive file to extract.
183
   * @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}.
184
   * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is moved to {@code targetDir}.
185
   */
186
  default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook) {
187

188
    extract(archiveFile, targetDir, postExtractHook, true);
6✔
189
  }
1✔
190

191
  /**
192
   * @param archiveFile the {@link Path} to the archive file to extract.
193
   * @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}.
194
   * @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is moved to {@code targetDir}.
195
   * @param extract {@code true} if the {@code archiveFile} should be extracted (default), {@code false} otherwise.
196
   */
197
  void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract);
198

199
  /**
200
   * Extracts a ZIP file what is the common archive format on Windows. Initially invented by PKZIP for MS-DOS and also famous from WinZIP software for Windows.
201
   *
202
   * @param file the ZIP file to extract.
203
   * @param targetDir the {@link Path} with the directory to unzip to.
204
   */
205
  void extractZip(Path file, Path targetDir);
206

207
  /**
208
   * @param file the ZIP file to extract.
209
   * @param targetDir the {@link Path} with the directory to unzip to.
210
   * @param compression the {@link TarCompression} to use.
211
   */
212
  void extractTar(Path file, Path targetDir, TarCompression compression);
213

214
  /**
215
   * @param file the JAR file to extract.
216
   * @param targetDir the {@link Path} with the directory to extract to.
217
   */
218
  void extractJar(Path file, Path targetDir);
219

220
  /**
221
   * Extracts an Apple DMG (Disk Image) file that is similar to an ISO image. DMG files are commonly used for software releases on MacOS. Double-clicking such
222
   * files on MacOS mounts them and show the application together with a symbolic link to the central applications folder and some help instructions. The user
223
   * then copies the application to the applications folder via drag and drop in order to perform the installation.
224
   *
225
   * @param file the DMG file to extract.
226
   * @param targetDir the target directory where to extract the contents to.
227
   */
228
  void extractDmg(Path file, Path targetDir);
229

230
  /**
231
   * Extracts an MSI (Microsoft Installer) file. MSI files are commonly used for software releases on Windows that allow an installation wizard and easy later
232
   * uninstallation.
233
   *
234
   * @param file the MSI file to extract.
235
   * @param targetDir the target directory where to extract the contents to.
236
   */
237
  void extractMsi(Path file, Path targetDir);
238

239
  /**
240
   * Extracts an Apple PKG (Package) file. PKG files are used instead of {@link #extractDmg(Path, Path) DMG files} if additional changes have to be performed
241
   * like drivers to be installed. Similar to what {@link #extractMsi(Path, Path) MSI} is on Windows. PKG files are internally a xar based archive with a
242
   * specific structure.
243
   *
244
   * @param file the PKG file to extract.
245
   * @param targetDir the target directory where to extract the contents to.
246
   */
247
  void extractPkg(Path file, Path targetDir);
248

249
  /**
250
   * @param dir the {@link Path directory} to compress.
251
   * @param out the {@link OutputStream} to write the compressed data to.
252
   * @param format the path, filename or extension to derive the archive format from (e.g. "tgz", "tar.gz", "zip", etc.).
253
   */
254
  void compress(Path dir, OutputStream out, String format);
255

256
  /**
257
   * @param dir the {@link Path directory} to compress as TAR with given {@link TarCompression}.
258
   * @param out the {@link OutputStream} to write the compressed data to.
259
   * @param tarCompression the {@link TarCompression} to use for the TAR archive.
260
   */
261
  void compressTar(Path dir, OutputStream out, TarCompression tarCompression);
262

263
  /**
264
   * @param dir the {@link Path directory} to compress as TAR.
265
   * @param out the {@link OutputStream} to write the compressed data to.
266
   */
267
  void compressTar(Path dir, OutputStream out);
268

269
  /**
270
   * @param dir the {@link Path directory} to compress as TGZ.
271
   * @param out the {@link OutputStream} to write the compressed data to.
272
   */
273
  void compressTarGz(Path dir, OutputStream out);
274

275
  /**
276
   * @param dir the {@link Path directory} to compress as TBZ2.
277
   * @param out the {@link OutputStream} to write the compressed data to.
278
   */
279
  void compressTarBzip2(Path dir, OutputStream out);
280

281
  /**
282
   * @param dir the {@link Path directory} to compress as ZIP.
283
   * @param out the {@link OutputStream} to write the compressed data to.
284
   */
285
  void compressZip(Path dir, OutputStream out);
286

287
  /**
288
   * @param path the {@link Path} to convert.
289
   * @return the absolute and physical {@link Path} (without symbolic links).
290
   */
291
  Path toRealPath(Path path);
292

293
  /**
294
   * @param path the {@link Path} to convert.
295
   * @return the absolute and physical {@link Path}.
296
   */
297
  Path toCanonicalPath(Path path);
298

299
  /**
300
   * Deletes the given {@link Path} idempotent and recursive.
301
   * <p>
302
   * ATTENTION: In most cases we want to use {@link #backup(Path)} instead to prevent the user from data loss.
303
   * </p>
304
   *
305
   * @param path the {@link Path} to delete.
306
   */
307
  void delete(Path path);
308

309
  /**
310
   * Creates a new temporary directory. ATTENTION: The user of this method is responsible to do house-keeping and {@link #delete(Path) delete} it after the work
311
   * is done.
312
   *
313
   * @param name the default name of the temporary directory to create. A prefix or suffix may be added to ensure uniqueness.
314
   * @return the {@link Path} to the newly created and unique temporary directory.
315
   */
316
  Path createTempDir(String name);
317

318
  /**
319
   * @param dir the folder to search.
320
   * @param filter the {@link Predicate} used to find the {@link Predicate#test(Object) match}.
321
   * @param recursive - {@code true} to search recursive in all sub-folders, {@code false} otherwise.
322
   * @return the first child {@link Path} matching the given {@link Predicate} or {@code null} if no match was found.
323
   */
324
  Path findFirst(Path dir, Predicate<Path> filter, boolean recursive);
325

326
  /**
327
   * @param dir the {@link Path} to the directory where to list the children.
328
   * @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if {@code true} is returned).
329
   * @return all children of the given {@link Path} that match the given {@link Predicate}. Will be the empty list of the given {@link Path} is not an existing
330
   *     directory.
331
   */
332
  default List<Path> listChildren(Path dir, Predicate<Path> filter) {
333

334
    return listChildrenMapped(dir, child -> (filter.test(child)) ? child : null);
13!
335
  }
336

337
  /**
338
   * @param dir the {@link Path} to the directory where to list the children.
339
   * @param filter the filter {@link Function} used to {@link Function#apply(Object) filter and transform} children to include. If the {@link Function}
340
   *     returns  {@code null}, the child will be filtered, otherwise the returned {@link Path} will be included in the resulting {@link List}.
341
   * @return all children of the given {@link Path} returned by the given {@link Function}. Will be the empty list if the given {@link Path} is not an existing
342
   *     directory.
343
   */
344
  List<Path> listChildrenMapped(Path dir, Function<Path, Path> filter);
345

346
  /**
347
   * Finds the existing file with the specified name in the given list of directories.
348
   *
349
   * @param fileName The name of the file to find.
350
   * @param searchDirs The list of directories to search for the file.
351
   * @return The {@code Path} of the existing file, or {@code null} if the file is not found.
352
   */
353
  Path findExistingFile(String fileName, List<Path> searchDirs);
354

355
  /**
356
   * Checks if the given directory is empty.
357
   *
358
   * @param dir The {@link Path} object representing the directory to check.
359
   * @return {@code true} if the directory is empty, {@code false} otherwise.
360
   */
361
  boolean isEmptyDir(Path dir);
362

363
  /**
364
   * Sets or unsets the writable permission for the specified file path.
365
   *
366
   * @param file {@link Path} to the file.
367
   * @param writable {@code true} to make the file writable, {@code false} to make it read-only
368
   * @return {@code true} if the operation was successful or supported, {@code false} otherwise
369
   */
370
  boolean setWritable(Path file, boolean writable);
371

372
  /**
373
   * Makes a path executable (analog to 'chmod a+x').
374
   *
375
   * @param path the {@link Path} to the file or directory.
376
   */
377
  default void makeExecutable(Path path) {
378

379
    makeExecutable(path, false);
4✔
380
  }
1✔
381

382
  /**
383
   * Makes a path executable (analog to 'chmod a+x').
384
   *
385
   * @param path the {@link Path} to the file or directory.
386
   * @param confirm - {@code true} to get user confirmation before adding missing executable flags, {@code false} otherwise (always set missing flags).
387
   */
388
  void makeExecutable(Path path, boolean confirm);
389

390
  /**
391
   * Sets the given {@link PathPermissions} for the specified {@link Path}.
392
   *
393
   * @param path the {@link Path} to the file or directory.
394
   * @param permissions the {@link PathPermissions} to set.
395
   * @param logErrorAndContinue - {@code true} to only log errors and continue, {@code false} to fail with an exception on error.
396
   */
397
  void setFilePermissions(Path path, PathPermissions permissions, boolean logErrorAndContinue);
398

399
  /**
400
   * Like the linux touch command this method will update the modification time of the given {@link Path} to the current
401
   * {@link System#currentTimeMillis() system time}. In case the file does not exist, it will be created as empty file. If already the
402
   * {@link Path#getParent() parent folder} does not exist, the operation will fail.
403
   *
404
   * @param file the {@link Path} to the file or folder.
405
   */
406
  void touch(Path file);
407

408
  /**
409
   * @param file the {@link Path} to the file to read.
410
   * @return the content of the specified file (in UTF-8 encoding), or null if the file doesn't exist
411
   * @see java.nio.file.Files#readString(Path)
412
   */
413
  String readFileContent(Path file);
414

415
  /**
416
   * @param content the {@link String} with the text to write to a file.
417
   * @param file the {@link Path} to the file where to save.
418
   */
419
  default void writeFileContent(String content, Path file) {
420

421
    writeFileContent(content, file, false);
5✔
422
  }
1✔
423

424
  /**
425
   * @param content the {@link String} with the text to write to a file.
426
   * @param file the {@link Path} to the file where to save.
427
   * @param createParentDir if {@code true}, the parent directory will be created if it does not already exist, {@code false} otherwise (fail if parent does
428
   *     not exist).
429
   */
430
  void writeFileContent(String content, Path file, boolean createParentDir);
431

432
  /**
433
   * Like {@link #readFileContent(Path)} but giving one {@link String} per line of text. It will not allow to preserve line endings (CRLF vs. LF).
434
   *
435
   * @param file the {@link Path} to the file to read.
436
   * @return the content of the specified file (in UTF-8 encoding) as {@link List} of {@link String}s per line of text.
437
   */
438
  List<String> readFileLines(Path file);
439

440
  /**
441
   * Like {@link #writeFileContent(String, Path)} but taking a {@link List} with one {@link String} per line of text. It will always use LF as newline character
442
   * independent of the operating system.
443
   *
444
   * @param lines the {@link List} of {@link String}s per line of text.
445
   * @param file the {@link Path} to the file where to save.
446
   */
447
  default void writeFileLines(List<String> lines, Path file) {
448
    writeFileLines(lines, file, false);
5✔
449
  }
1✔
450

451
  /**
452
   * Like {@link #writeFileContent(String, Path, boolean)} but taking a {@link List} with one {@link String} per line of text. It will always use LF as newline
453
   * character independent of the operating system.
454
   *
455
   * @param lines the {@link List} of {@link String}s per line of text.
456
   * @param file the {@link Path} to the file where to save.
457
   * @param createParentDir if {@code true}, the parent directory will be created if it does not already exist, {@code false} otherwise (fail if parent does
458
   *     not exist).
459
   */
460
  void writeFileLines(List<String> lines, Path file, boolean createParentDir);
461

462
  /**
463
   * @param path the {@link Path} to check.
464
   * @return {@code true} if the given {@link Path} is a junction, false otherwise.
465
   */
466
  boolean isJunction(Path path);
467

468
  /**
469
   * @param file the {@link Path} to the {@link Properties} file to read.
470
   * @return the parsed {@link Properties}.
471
   */
472
  default Properties readProperties(Path file) {
473
    Properties properties = new Properties();
4✔
474
    readProperties(file, properties);
4✔
475
    return properties;
2✔
476
  }
477

478
  /**
479
   * @param file the {@link Path} to the {@link Properties} file to read.
480
   * @param properties the existing {@link Properties} to {@link Properties#load(Reader) load} into.
481
   */
482
  void readProperties(Path file, Properties properties);
483

484
  /**
485
   * @param properties the {@link Properties} to save.
486
   * @param file the {@link Path} to the file where to save the properties.
487
   */
488
  default void writeProperties(Properties properties, Path file) {
489

490
    writeProperties(properties, file, false);
5✔
491
  }
1✔
492

493

494
  /**
495
   * @param properties the {@link Properties} to save.
496
   * @param file the {@link Path} to the file where to save the properties.
497
   * @param createParentDir if {@code true}, the parent directory will created if it does not already exist, {@code false} otherwise (fail if parent does
498
   *     not exist).
499
   */
500
  void writeProperties(Properties properties, Path file, boolean createParentDir);
501

502
  /**
503
   * @param file the {@link Path} to read from
504
   * @return {@link com.devonfw.tools.ide.io.ini.IniFile}
505
   */
506
  default IniFile readIniFile(Path file) {
507
    IniFile iniFile = new IniFileImpl();
4✔
508
    readIniFile(file, iniFile);
4✔
509
    return iniFile;
2✔
510
  }
511

512
  /**
513
   * @param file the {@link Path} to read from
514
   * @param iniFile the {@link IniFile} object the data is loaded into
515
   */
516
  void readIniFile(Path file, IniFile iniFile);
517

518
  /**
519
   * @param iniFile the {@link IniFile} object
520
   * @param file the {@link Path} to write to
521
   */
522
  default void writeIniFile(IniFile iniFile, Path file) {
523
    writeIniFile(iniFile, file, false);
5✔
524
  }
1✔
525

526
  /**
527
   * @param iniFile the {@link IniFile} object
528
   * @param file the {@link Path} to write to
529
   * @param createParentDir whether to create missing parent directories
530
   */
531
  void writeIniFile(IniFile iniFile, Path file, boolean createParentDir);
532

533
  /**
534
   * @param path the {@link Path} to get the age from the modification time.
535
   * @return the age of the file as {@link Duration} from now to the modification time of the file.
536
   */
537
  public Duration getFileAge(Path path);
538

539
  /**
540
   * @param path the {@link Path} to check.
541
   * @param cacheDuration the {@link Duration} to consider as recent.
542
   * @return {@code true} if the given {@link Path} exists and is recent enough (its {@link #getFileAge(Path) age} is not greater than the given
543
   *     {@link Duration}), {@code false} otherwise.
544
   */
545
  boolean isFileAgeRecent(Path path, Duration cacheDuration);
546
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc