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

CyclopsMC / CyclopsCore / #479033852

06 Nov 2026 12:51PM UTC coverage: 23.052% (+0.005%) from 23.047%
#479033852

Pull #259

github

web-flow
Merge b42213c76 into b345f68bf
Pull Request #259: Fix packets failing to decode when a map is modified while encoding, and log codec errors

2 of 6 new or added lines in 2 files covered. (33.33%)

28 existing lines in 1 file now uncovered.

2444 of 10602 relevant lines covered (23.05%)

0.23 hits per line

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

67.83
/src/main/java/org/cyclops/cyclopscore/network/PacketCodec.java
1
package org.cyclops.cyclopscore.network;
2

3
import com.google.common.collect.Lists;
4
import com.google.common.collect.Maps;
5
import net.minecraft.core.BlockPos;
6
import net.minecraft.core.Direction;
7
import net.minecraft.core.Vec3i;
8
import net.minecraft.nbt.CompoundTag;
9
import net.minecraft.nbt.Tag;
10
import net.minecraft.network.FriendlyByteBuf;
11
import net.minecraft.resources.ResourceKey;
12
import net.minecraft.resources.ResourceLocation;
13
import net.minecraft.world.item.ItemStack;
14
import net.minecraft.world.phys.Vec3;
15
import net.minecraftforge.fluids.FluidStack;
16
import org.apache.commons.lang3.ClassUtils;
17
import org.apache.commons.lang3.tuple.Pair;
18
import org.cyclops.cyclopscore.datastructure.DimPos;
19
import org.cyclops.cyclopscore.datastructure.SingleCache;
20
import org.cyclops.cyclopscore.inventory.ItemLocation;
21
import org.joml.Vector3d;
22

23
import javax.annotation.Nullable;
24
import java.lang.reflect.Field;
25
import java.util.Arrays;
26
import java.util.Collections;
27
import java.util.Comparator;
28
import java.util.List;
29
import java.util.Map;
30
import java.util.Objects;
31

32
/**
33
 * Packet with automatic coding and decoding of basic fields annotated with {@link CodecField}.
34
 * @author rubensworks
35
 *
36
 */
37
public abstract class PacketCodec extends PacketBase {
1 ✔
38

39
    public static final int READ_STRING_MAX_LENGTH = 32767;
40

41
    private static Map<Class<?>, ICodecAction> codecActions = Maps.newHashMap();
1 ✔
42
    static {
43
        codecActions.put(String.class, new ICodecAction() {
1 ✔
44

45
            @Override
46
            public void encode(Object object, FriendlyByteBuf output) {
47
                output.writeUtf((String) object);
1 ✔
48
            }
1 ✔
49

50
            @Override
51
            public Object decode(FriendlyByteBuf input) {
52
                return input.readUtf(32767); // The arg-less version is client-side only, so we copy its implementation.
1 ✔
53
            }
54
        });
55

56
        codecActions.put(double.class, new ICodecAction() {
1 ✔
57

58
            @Override
59
            public void encode(Object object, FriendlyByteBuf output) {
60
                output.writeDouble((Double) object);
1 ✔
61
            }
1 ✔
62

63
            @Override
64
            public Object decode(FriendlyByteBuf input) {
65
                return input.readDouble();
1 ✔
66
            }
67
        });
68

69
        codecActions.put(int.class, new ICodecAction() {
1 ✔
70

71
            @Override
72
            public void encode(Object object, FriendlyByteBuf output) {
73
                output.writeInt((int) object);
1 ✔
74
            }
1 ✔
75

76
            @Override
77
            public Object decode(FriendlyByteBuf input) {
78
                return input.readInt();
1 ✔
79
            }
80
        });
81

82
        codecActions.put(long.class, new ICodecAction() {
1 ✔
83

84
            @Override
85
            public void encode(Object object, FriendlyByteBuf output) {
86
                output.writeLong((long) object);
1 ✔
87
            }
1 ✔
88

89
            @Override
90
            public Object decode(FriendlyByteBuf input) {
91
                return input.readLong();
1 ✔
92
            }
93
        });
94

95
        codecActions.put(short.class, new ICodecAction() {
1 ✔
96

97
            @Override
98
            public void encode(Object object, FriendlyByteBuf output) {
99
                output.writeShort((Short) object);
1 ✔
100
            }
1 ✔
101

102
            @Override
103
            public Object decode(FriendlyByteBuf input) {
104
                return input.readShort();
1 ✔
105
            }
106
        });
107

108
        codecActions.put(boolean.class, new ICodecAction() {
1 ✔
109

110
            @Override
111
            public void encode(Object object, FriendlyByteBuf output) {
112
                output.writeBoolean((Boolean) object);
1 ✔
113
            }
1 ✔
114

115
            @Override
116
            public Object decode(FriendlyByteBuf input) {
117
                return input.readBoolean();
1 ✔
118
            }
119
        });
120

121
        codecActions.put(float.class, new ICodecAction() {
1 ✔
122

123
            @Override
124
            public void encode(Object object, FriendlyByteBuf output) {
125
                output.writeFloat((Float) object);
1 ✔
126
            }
1 ✔
127

128
            @Override
129
            public Object decode(FriendlyByteBuf input) {
130
                return input.readFloat();
1 ✔
131
            }
132
        });
133

134
        codecActions.put(Vector3d.class, new ICodecAction() {
1 ✔
135
            @Override
136
            public void encode(Object object, FriendlyByteBuf output) {
137
                Vector3d v = (Vector3d)object;
1 ✔
138
                output.writeDouble(v.x);
1 ✔
139
                output.writeDouble(v.y);
1 ✔
140
                output.writeDouble(v.z);
1 ✔
141
            }
1 ✔
142

143
            @Override
144
            public Object decode(FriendlyByteBuf input) {
145
                double x = input.readDouble();
1 ✔
146
                double y = input.readDouble();
1 ✔
147
                double z = input.readDouble();
1 ✔
148
                return new Vector3d(x, y, z);
1 ✔
149
            }
150
        });
151

152
        codecActions.put(Vec3i.class, new ICodecAction() {
1 ✔
153
            @Override
154
            public void encode(Object object, FriendlyByteBuf output) {
155
                Vec3i v = (Vec3i)object;
×
156
                output.writeInt(v.getX());
×
157
                output.writeInt(v.getY());
×
158
                output.writeInt(v.getZ());
×
159
            }
×
160

161
            @Override
162
            public Object decode(FriendlyByteBuf input) {
163
                int x = input.readInt();
×
164
                int y = input.readInt();
×
165
                int z = input.readInt();
×
166
                return new Vec3i(x, y, z);
×
167
            }
168
        });
169

170
        codecActions.put(Vec3.class, new ICodecAction() {
1 ✔
171
            @Override
172
            public void encode(Object object, FriendlyByteBuf output) {
173
                Vec3 v = (Vec3)object;
×
174
                output.writeDouble(v.x);
×
175
                output.writeDouble(v.y);
×
176
                output.writeDouble(v.z);
×
177
            }
×
178

179
            @Override
180
            public Object decode(FriendlyByteBuf input) {
181
                double x = input.readDouble();
×
182
                double y = input.readDouble();
×
183
                double z = input.readDouble();
×
184
                return new Vec3(x, y, z);
×
185
            }
186
        });
187

188
        codecActions.put(Map.class, new ICodecAction() {
1 ✔
189

190
            // Packet structure:
191
            // Map length (int)
192
            // --- end if length == 0
193
            // Key class name (UTF)
194
            // Value class name (UTF)
195
            // for length
196
            //   key
197
            //   value
198

199
            @SuppressWarnings({ "rawtypes", "unchecked" })
200
            @Override
201
            public void encode(Object object, FriendlyByteBuf output) {
202
                Map map = (Map) object;
1 ✔
203
                // Snapshot, so the written size matches the written entries if the map is modified concurrently.
204
                List<Map.Entry> entries = Lists.newArrayList(map.entrySet());
1 ✔
205
                output.writeInt(entries.size());
1 ✔
206
                ICodecAction keyAction = null;
1 ✔
207
                ICodecAction valueAction = null;
1 ✔
208
                for(Map.Entry entry : entries) {
1 ✔
209
                    if(keyAction == null) {
1 ✔
210
                        keyAction = getAction(entry.getKey().getClass());
1 ✔
211
                        output.writeUtf(entry.getKey().getClass().getName());
1 ✔
212
                    }
213
                    if(valueAction == null) {
1 ✔
214
                        valueAction = getAction(entry.getValue().getClass());
1 ✔
215
                        output.writeUtf(entry.getValue().getClass().getName());
1 ✔
216
                    }
217
                    keyAction.encode(entry.getKey(), output);
1 ✔
218
                    valueAction.encode(entry.getValue(), output);
1 ✔
219
                }
1 ✔
220
            }
1 ✔
221

222
            @SuppressWarnings({ "rawtypes", "unchecked" })
223
            @Override
224
            public Object decode(FriendlyByteBuf input) {
225
                Map map = Maps.newHashMap();
1 ✔
226
                int size = input.readInt();
1 ✔
227
                if(size == 0) {
1 ✔
228
                    return map;
1 ✔
229
                }
230
                try {
231
                    ICodecAction keyAction = getAction(Class.forName(input.readUtf(READ_STRING_MAX_LENGTH)));
1 ✔
232
                    ICodecAction valueAction = getAction(Class.forName(input.readUtf(READ_STRING_MAX_LENGTH)));
1 ✔
233
                    for(int i = 0; i < size; i++) {
1 ✔
234
                        Object key = keyAction.decode(input);
1 ✔
235
                        Object value = valueAction.decode(input);
1 ✔
236
                        map.put(key, value);
1 ✔
237
                    }
238
                } catch (ClassNotFoundException e) {
×
UNCOV
239
                    e.printStackTrace();
×
240
                }
1 ✔
241
                return map;
1 ✔
242
            }
243
        });
244

245
        codecActions.put(CompoundTag.class, new ICodecAction() {
1 ✔
246

247
            @Override
248
            public void encode(Object object, FriendlyByteBuf output) {
249
                output.writeNbt((CompoundTag) object);
1 ✔
250
            }
1 ✔
251

252
            @Override
253
            public Object decode(FriendlyByteBuf input) {
254
                return input.readNbt();
1 ✔
255
            }
256
        });
257

258
        codecActions.put(Tag.class, new ICodecAction() {
1 ✔
259

260
            @Override
261
            public void encode(Object object, FriendlyByteBuf output) {
262
                CompoundTag tag = new CompoundTag();
×
263
                tag.put("v", (Tag) object);
×
264
                output.writeNbt(tag);
×
UNCOV
265
            }
×
266

267
            @Override
268
            public Object decode(FriendlyByteBuf input) {
269
                CompoundTag tag = input.readNbt();
×
UNCOV
270
                return tag.get("v");
×
271
            }
272
        });
273

274
        codecActions.put(ItemStack.class, new ICodecAction() {
1 ✔
275

276
            @Override
277
            public void encode(Object object, FriendlyByteBuf output) {
278
                output.writeItem((ItemStack) object);
×
UNCOV
279
            }
×
280

281
            @Override
282
            public Object decode(FriendlyByteBuf input) {
UNCOV
283
                return input.readItem();
×
284
            }
285
        });
286

287
        codecActions.put(FluidStack.class, new ICodecAction() {
1 ✔
288

289
            @Override
290
            public void encode(Object object, FriendlyByteBuf output) {
291
                output.writeNbt(((FluidStack) object).writeToNBT(new CompoundTag()));
×
UNCOV
292
            }
×
293

294
            @Override
295
            public Object decode(FriendlyByteBuf input) {
UNCOV
296
                return FluidStack.loadFluidStackFromNBT(input.readNbt());
×
297
            }
298
        });
299

300
        codecActions.put(Direction.class, new ICodecAction() {
1 ✔
301

302
            @Override
303
            public void encode(Object object, FriendlyByteBuf output) {
304
                output.writeInt(((Direction) object).ordinal());
1 ✔
305
            }
1 ✔
306

307
            @Override
308
            public Object decode(FriendlyByteBuf input) {
309
                return Direction.values()[input.readInt()];
1 ✔
310
            }
311
        });
312

313
        codecActions.put(BlockPos.class, new ICodecAction() {
1 ✔
314

315
            @Override
316
            public void encode(Object object, FriendlyByteBuf output) {
317
                output.writeBlockPos((BlockPos) object);
1 ✔
318
            }
1 ✔
319

320
            @Override
321
            public Object decode(FriendlyByteBuf input) {
322
                return input.readBlockPos();
1 ✔
323
            }
324
        });
325

326
        codecActions.put(ResourceKey.class, new ICodecAction() {
1 ✔
327

328
            @Override
329
            public void encode(Object object, FriendlyByteBuf output) {
330
                output.writeUtf(((ResourceKey) object).registry().toString());
×
331
                output.writeUtf(((ResourceKey) object).location().toString());
×
UNCOV
332
            }
×
333

334
            @Override
335
            public Object decode(FriendlyByteBuf input) {
336
                return ResourceKey.create(
×
337
                        ResourceKey.createRegistryKey(new ResourceLocation(input.readUtf(READ_STRING_MAX_LENGTH))),
×
UNCOV
338
                        new ResourceLocation(input.readUtf(READ_STRING_MAX_LENGTH)));
×
339
            }
340
        });
341

342
        codecActions.put(DimPos.class, new ICodecAction() {
1 ✔
343

344
            @Override
345
            public void encode(Object object, FriendlyByteBuf output) {
346
                write(output, ((DimPos) object).getLevelKey());
×
347
                write(output, ((DimPos) object).getBlockPos());
×
UNCOV
348
            }
×
349

350
            @Override
351
            public Object decode(FriendlyByteBuf input) {
352
                ResourceKey dimensionType = read(input, ResourceKey.class);
×
353
                BlockPos blockPos = read(input, BlockPos.class);
×
UNCOV
354
                return DimPos.of(dimensionType, blockPos);
×
355
            }
356
        });
357

358
        codecActions.put(List.class, new ICodecAction() {
1 ✔
359

360
            // Packet structure:
361
            // list length (int)
362
            // --- end if length == 0
363
            // Value class name (UTF)
364
            //         id + value
365
            // -1
366

367
            @Override
368
            public void encode(Object object, FriendlyByteBuf output) {
369
                List<?> list = (List<?>) object;
1 ✔
370
                output.writeInt(list.size());
1 ✔
371
                if(list.size() == 0) return;
1 ✔
372
                ICodecAction valueAction = null;
1 ✔
373
                for(int i = 0; i < list.size(); i++) {
1 ✔
374
                    Object value = list.get(i);
1 ✔
375
                    if(value != null) {
1 ✔
376
                        if (valueAction == null) {
1 ✔
377
                            valueAction = getAction(value.getClass());
1 ✔
378
                            output.writeUtf(value.getClass().getName());
1 ✔
379
                        }
380
                        output.writeInt(i);
1 ✔
381
                        valueAction.encode(value, output);
1 ✔
382
                    }
383
                }
384
                if(valueAction == null) {
1 ✔
UNCOV
385
                    output.writeUtf("__noclass");
×
386
                } else {
387
                    output.writeInt(-1);
1 ✔
388
                }
389
            }
1 ✔
390

391
            @SuppressWarnings({ "rawtypes", "unchecked" })
392
            @Override
393
            public Object decode(FriendlyByteBuf input) {
394
                List list;
395
                int size = input.readInt();
1 ✔
396
                if(size == 0) {
1 ✔
397
                    return Collections.emptyList();
1 ✔
398
                } else {
399
                    list = Lists.newArrayListWithExpectedSize(size);
1 ✔
400
                }
401
                try {
402
                    String className = input.readUtf(READ_STRING_MAX_LENGTH);
1 ✔
403
                    if(!className.equals("__noclass")) {
1 ✔
404
                        ICodecAction valueAction = getAction(Class.forName(className));
1 ✔
405
                        int i, currentLength = 0;
1 ✔
406
                        while((i = input.readInt()) >= 0) {
1 ✔
407
                            while(currentLength < i) {
1 ✔
408
                                list.add(null);
1 ✔
409
                                currentLength++;
1 ✔
410
                            }
411
                            Object value = valueAction.decode(input);
1 ✔
412
                            list.add(value);
1 ✔
413
                            currentLength++;
1 ✔
414
                        }
1 ✔
415
                        while(currentLength < size) {
1 ✔
416
                            list.add(null);
1 ✔
417
                            currentLength++;
1 ✔
418
                        }
419
                    } else {
1 ✔
420
                        for (int i = 0; i < size; i++) {
×
UNCOV
421
                            list.add(null);
×
422
                        }
423
                    }
424
                } catch (ClassNotFoundException e) {
×
UNCOV
425
                    e.printStackTrace();
×
426
                }
1 ✔
427
                return list;
1 ✔
428
            }
429
        });
430

431
        codecActions.put(Pair.class, new ICodecAction() {
1 ✔
432

433
            @Override
434
            public void encode(Object object, FriendlyByteBuf output) {
435
                output.writeUtf(((Pair) object).getLeft().getClass().getName());
×
436
                output.writeUtf(((Pair) object).getRight().getClass().getName());
×
437
                write(output, ((Pair) object).getLeft());
×
438
                write(output, ((Pair) object).getRight());
×
UNCOV
439
            }
×
440

441
            @Override
442
            public Object decode(FriendlyByteBuf input) {
443
                try {
444
                    ICodecAction keyAction = getAction(Class.forName(input.readUtf(READ_STRING_MAX_LENGTH)));
×
445
                    ICodecAction valueAction = getAction(Class.forName(input.readUtf(READ_STRING_MAX_LENGTH)));
×
446
                    Object key = keyAction.decode(input);
×
447
                    Object value = valueAction.decode(input);
×
448
                    return Pair.of(key, value);
×
449
                } catch (ClassNotFoundException e) {
×
UNCOV
450
                    e.printStackTrace();
×
451
                }
UNCOV
452
                return Pair.of(null, null);
×
453
            }
454
        });
455

456
        codecActions.put(ItemLocation.class, new ICodecAction() {
1 ✔
457

458
            @Override
459
            public void encode(Object object, FriendlyByteBuf output) {
460
                ItemLocation.writeToPacketBuffer(output, (ItemLocation) object);
×
UNCOV
461
            }
×
462

463
            @Override
464
            public Object decode(FriendlyByteBuf input) {
UNCOV
465
                return ItemLocation.readFromPacketBuffer(input);
×
466
            }
467
        });
468
    }
1 ✔
469

470
    /**
471
     * Register a new coded action.
472
     * @param clazz A class type.
473
     * @param action A codec action for the given type.
474
     */
475
    public static void addCodedAction(Class<?> clazz, ICodecAction action) {
476
        codecActions.put(clazz, action);
×
UNCOV
477
    }
×
478

479
    protected SingleCache<Void, List<Field>> fieldCache = new SingleCache<Void, List<Field>>(
1 ✔
480
            new SingleCache.ICacheUpdater<Void, List<Field>>() {
1 ✔
481

482
        @Override
483
        public List<Field> getNewValue(Void key) {
484
            List<Field> fieldList = Lists.newLinkedList();
1 ✔
485

486
            Class<?> clazz = PacketCodec.this.getClass();
1 ✔
487
            for (; clazz != PacketCodec.class && clazz != null; clazz = clazz.getSuperclass()) {
1 ✔
488
                Field[] fields = clazz.getDeclaredFields();
1 ✔
489

490
                // Sort this because the Java API tells us that getDeclaredFields()
491
                // does not deterministically define the order of the fields in the array.
492
                // Otherwise we might get nasty class cast exceptions when running in SMP.
493
                Arrays.sort(fields, new Comparator<Field>() {
1 ✔
494

495
                    @Override
496
                    public int compare(Field o1, Field o2) {
497
                        return o1.getName().compareTo(o2.getName());
1 ✔
498
                    }
499

500
                });
501

502
                for (final Field field : fields) {
1 ✔
503
                    if (field.isAnnotationPresent(CodecField.class)) {
1 ✔
504
                        fieldList.add(field);
1 ✔
505
                    }
506
                }
507
            }
508

509
            return fieldList;
1 ✔
510
        }
511

512
        @Override
513
        public boolean isKeyEqual(Void cacheKey, Void newKey) {
UNCOV
514
            return true;
×
515
        }
516

517
    });
518

519
    @Nullable
520
    protected static ICodecAction getActionSuper(Class<?> clazz) {
521
        if(ClassUtils.isPrimitiveWrapper(clazz)) {
1 ✔
522
            clazz = ClassUtils.wrapperToPrimitive(clazz);
1 ✔
523
        }
524
        ICodecAction action = codecActions.get(clazz);
1 ✔
525
        if(action == null) {
1 ✔
526
            for (Class<?> iface : clazz.getInterfaces()) {
1 ✔
527
                action = codecActions.get(iface);
1 ✔
528
                if (action != null) {
1 ✔
529
                    return action;
1 ✔
530
                }
531
            }
532
            Class<?> superClass = clazz.getSuperclass();
×
533
            if (superClass != null) {
×
UNCOV
534
                return getActionSuper(superClass);
×
535
            } else {
UNCOV
536
                return null;
×
537
            }
538
        }
539
        return action;
1 ✔
540
    }
541

542
    public static ICodecAction getAction(Class<?> clazz) {
543
        ICodecAction action = getActionSuper(clazz);
1 ✔
544
        if(action == null) {
1 ✔
UNCOV
545
            System.err.println("No ICodecAction was found for " + clazz
×
546
                    + ". You should add one in PacketCodec.");
547
        }
548
        return action;
1 ✔
549
    }
550

551
    private void loopCodecFields(ICodecRunnable runnable) {
552
        for (Field field : fieldCache.get(null)) {
1 ✔
553
            Class<?> clazz = field.getType();
1 ✔
554
            ICodecAction action = getAction(clazz);
1 ✔
555

556
            // Make private fields temporarily accessible.
557
            boolean accessible = field.isAccessible();
1 ✔
558
            if (!accessible) {
1 ✔
559
                field.setAccessible(true);
1 ✔
560
            }
561
            runnable.run(field, action);
1 ✔
562
        }
1 ✔
563
    }
1 ✔
564

565
    @Override
566
    public void encode(final FriendlyByteBuf output) {
567
        loopCodecFields((field, action) -> {
1 ✔
568
            Object object = null;
1 ✔
569
            try {
570
                object = field.get(PacketCodec.this);
1 ✔
571
            } catch (IllegalArgumentException | IllegalAccessException e) {
×
UNCOV
572
                e.printStackTrace();
×
573
            }
1 ✔
574
            action.encode(object, output);
1 ✔
575
        });
1 ✔
576
    }
1 ✔
577

578
    @Override
579
    public void decode(final FriendlyByteBuf input) {
580
        loopCodecFields((field, action) -> {
1 ✔
581
            Object object = action.decode(input);
1 ✔
582
            try {
583
                field.set(PacketCodec.this, object);
1 ✔
584
            } catch (IllegalArgumentException | IllegalAccessException e) {
×
UNCOV
585
                e.printStackTrace();
×
586
            }
1 ✔
587
        });
1 ✔
588
    }
1 ✔
589

590
    /**
591
     * Write the given object into the packet buffer.
592
     * @param packetBuffer A packet buffer.
593
     * @param object An object.
594
     */
595
    public static void write(FriendlyByteBuf packetBuffer, Object object) {
596
        ICodecAction action = Objects.requireNonNull(getActionSuper(object.getClass()),
×
597
                "No codec action was registered for " + object.getClass().getName());
×
598
        action.encode(object, packetBuffer);
×
UNCOV
599
    }
×
600

601
    /**
602
     * Read the an object of the given type from the packet buffer.
603
     * @param packetBuffer A packet buffer.
604
     * @param clazz The class type to read.
605
     * @param <T> The type of object.
606
     * @return The read object.
607
     */
608
    public static <T> T read(FriendlyByteBuf packetBuffer, Class<T> clazz) {
609
        ICodecAction action = Objects.requireNonNull(getActionSuper(clazz));
×
UNCOV
610
        return (T) action.decode(packetBuffer);
×
611
    }
612

613
    public static interface ICodecAction {
614

615
        /**
616
         * Encode the given object.
617
         * @param object The object to encode into the output.
618
         * @param output The byte array to encode to.
619
         */
620
        public void encode(Object object, FriendlyByteBuf output);
621

622
        /**
623
         * Decode from the input.
624
         * @param input The byte array to decode from.
625
         * @return The object to return after reading it from the input.
626
         */
627
        public Object decode(FriendlyByteBuf input);
628

629
    }
630

631
    public static interface ICodecRunnable {
632

633
        /**
634
         * Run a type of codec.
635
         * @param field The field annotated with {@link CodecField}.
636
         * @param action The action that must be applied to the field.
637
         */
638
        public void run(Field field, ICodecAction action);
639

640
    }
641

642
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc