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

CyclopsMC / IntegratedDynamics / 13739195914

08 Mar 2025 03:58PM UTC coverage: 39.059% (+0.06%) from 39.002%
13739195914

push

github

rubensworks
Merge remote-tracking branch 'origin/master-1.21-lts' into master-1.21

1966 of 8373 branches covered (23.48%)

Branch coverage included in aggregate %.

10307 of 23049 relevant lines covered (44.72%)

2.1 hits per line

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

64.18
/src/main/java/org/cyclops/integrateddynamics/core/network/Network.java
1
package org.cyclops.integrateddynamics.core.network;
2

3
import com.google.common.collect.Lists;
4
import com.google.common.collect.Maps;
5
import com.google.common.collect.Sets;
6
import it.unimi.dsi.fastutil.objects.Object2IntAVLTreeMap;
7
import it.unimi.dsi.fastutil.objects.Object2IntMap;
8
import net.minecraft.core.BlockPos;
9
import net.minecraft.core.Direction;
10
import net.minecraft.core.HolderLookup;
11
import net.minecraft.nbt.CompoundTag;
12
import net.minecraft.world.level.Level;
13
import net.minecraft.world.level.block.state.BlockState;
14
import net.neoforged.fml.ModLoader;
15
import net.neoforged.neoforge.capabilities.ICapabilityProvider;
16
import org.cyclops.cyclopscore.datastructure.DimPos;
17
import org.cyclops.cyclopscore.helper.IModHelpersNeoForge;
18
import org.cyclops.integrateddynamics.Capabilities;
19
import org.cyclops.integrateddynamics.IntegratedDynamics;
20
import org.cyclops.integrateddynamics.api.PartStateException;
21
import org.cyclops.integrateddynamics.api.network.*;
22
import org.cyclops.integrateddynamics.api.network.event.INetworkEvent;
23
import org.cyclops.integrateddynamics.api.network.event.INetworkEventBus;
24
import org.cyclops.integrateddynamics.api.path.IPathElement;
25
import org.cyclops.integrateddynamics.api.path.ISidedPathElement;
26
import org.cyclops.integrateddynamics.capability.path.SidedPathElement;
27
import org.cyclops.integrateddynamics.core.network.diagnostics.NetworkDiagnostics;
28
import org.cyclops.integrateddynamics.core.network.event.NetworkElementAddEvent;
29
import org.cyclops.integrateddynamics.core.network.event.NetworkElementRemoveEvent;
30
import org.cyclops.integrateddynamics.core.network.event.NetworkEventBus;
31
import org.cyclops.integrateddynamics.core.network.event.VariableContentsUpdatedEvent;
32
import org.cyclops.integrateddynamics.core.path.Cluster;
33
import org.cyclops.integrateddynamics.core.path.PathFinder;
34
import org.cyclops.integrateddynamics.core.persist.world.NetworkWorldStorage;
35

36
import java.util.*;
37

38
/**
39
 * A network instance that can hold a set of {@link INetworkElement}s.
40
 * Note that this network only contains references to the relevant data, it does not contain the actual information.
41
 * @author rubensworks
42
 */
43
public class Network implements INetwork {
44

45
    private Cluster baseCluster;
46

47
    private final INetworkEventBus eventBus = new NetworkEventBus();
5✔
48
    private final TreeSet<INetworkElement> elements = Sets.newTreeSet();
3✔
49
    private Object2IntMap<INetworkElement> updateableElementsTicks = null;
3✔
50
    private TreeSet<INetworkElement> invalidatedElements = Sets.newTreeSet();
3✔
51
    private Map<INetworkElement, Long> lastSecondDurations = Maps.newHashMap();
3✔
52

53
    private Map<NetworkCapability<?>, List<ICapabilityProvider<INetwork, Void, ?>>> capabilityProviders;
54
    private IFullNetworkListener[] fullNetworkListeners;
55

56
    private CompoundTag toRead = null;
3✔
57
    private HolderLookup.Provider provider = null;
3✔
58
    private volatile boolean changed = false;
3✔
59
    private volatile boolean killed = false;
3✔
60

61
    private boolean crashed = false;
3✔
62

63
    /**
64
     * Initiate a full network from the given start position.
65
     * @param sidedPathElement The sided path element to start from.
66
     * @return The newly formed network.
67
     */
68
    public static Network initiateNetworkSetup(ISidedPathElement sidedPathElement) {
69
        Network network = new Network(PathFinder.getConnectedCluster(sidedPathElement));
6✔
70
        NetworkWorldStorage.getInstance(IntegratedDynamics._instance).addNewNetwork(network);
4✔
71
        return network;
2✔
72
    }
73

74
    /**
75
     * Check if two networks are equal.
76
     * @param networkA A network.
77
     * @param networkB Another network.
78
     * @return If they are equal.
79
     */
80
    public static boolean areNetworksEqual(Network networkA, Network networkB) {
81
        return networkA.elements.containsAll(networkB.elements) && networkA.elements.size() == networkB.elements.size();
×
82
    }
83

84
    /**
85
     * This constructor should not be called, except for the process of constructing networks from NBT.
86
     */
87
    public Network() {
×
88
        this.baseCluster = new Cluster();
×
89
        gatherCapabilities();
×
90
        onConstruct();
×
91
    }
×
92

93
    /**
94
     * Create a new network from a given cluster of path elements.
95
     * Each path element will be checked if it has a {@link INetworkElementProvider} capability at its position
96
     * and will add all its elements to the network in that case.
97
     * Each path element that has an {@link org.cyclops.integrateddynamics.api.part.IPartContainer} capability
98
     * will have the network stored in its part container.
99
     * @param pathElements The path elements that make up the connections in the network which can potentially provide network
100
     *               elements.
101
     */
102
    public Network(Cluster pathElements) {
2✔
103
        this.baseCluster = pathElements;
3✔
104
        gatherCapabilities();
2✔
105
        onConstruct();
2✔
106
        deriveNetworkElements(baseCluster);
4✔
107
    }
1✔
108

109
    protected void gatherCapabilities() {
110
        AttachCapabilitiesEventNetwork event = new AttachCapabilitiesEventNetwork(this);
5✔
111
        ModLoader.postEventWrapContainerInModOrder(event);
2✔
112
        List<IFullNetworkListener> listeners = event.getFullNetworkListeners();
3✔
113
        this.fullNetworkListeners = listeners.toArray(new IFullNetworkListener[listeners.size()]);
8✔
114
        this.capabilityProviders = event.getProviders();
4✔
115
    }
1✔
116

117
    protected IFullNetworkListener[] gatherFullNetworkListeners() {
118
        List<IFullNetworkListener> listeners = Lists.newArrayList();
×
119

120
        return listeners.toArray(new IFullNetworkListener[listeners.size()]);
×
121
    }
122

123
    protected void onConstruct() {
124

125
    }
1✔
126

127
    private void deriveNetworkElements(Cluster pathElements) {
128
        if(!killIfEmpty()) {
3!
129
            for (ISidedPathElement sidedPathElement : pathElements) {
10✔
130
                Level world = sidedPathElement.getPathElement().getPosition().getLevel(true);
6✔
131
                BlockPos pos = sidedPathElement.getPathElement().getPosition().getBlockPos();
5✔
132
                Direction side = sidedPathElement.getSide();
3✔
133
                IModHelpersNeoForge.get().getCapabilityHelpers().getCapability(world, pos, side, Capabilities.NetworkCarrier.BLOCK).ifPresent(networkCarrier -> {
14✔
134
                    // Correctly remove any previously saved network in this carrier
135
                    // and set the new network to this.
136
                    INetwork network = networkCarrier.getNetwork();
3✔
137
                    if (network != null) {
2✔
138
                        network.removePathElement(sidedPathElement.getPathElement(), side, world.getBlockState(pos));
9✔
139
                    }
140
                    networkCarrier.setNetwork(null);
3✔
141
                    networkCarrier.setNetwork(this);
3✔
142
                });
1✔
143
                IModHelpersNeoForge.get().getCapabilityHelpers().getCapability(world, pos, side, Capabilities.NetworkElementProvider.BLOCK).ifPresent(networkElementProvider -> {
12✔
144
                    for(INetworkElement element : networkElementProvider.createNetworkElements(world, pos)) {
13✔
145
                        addNetworkElement(element, true);
5✔
146
                    }
1✔
147
                });
1✔
148
            }
1✔
149
            onNetworkChanged();
2✔
150
        }
151
    }
1✔
152

153
    @Override
154
    public boolean isInitialized() {
155
        return updateableElementsTicks != null;
6!
156
    }
157

158
    @Override
159
    public INetworkEventBus getEventBus() {
160
        return this.eventBus;
3✔
161
    }
162

163
    /**
164
     * Initialize the network element data.
165
     */
166
    public void initialize() {
167
        initialize(false);
3✔
168
    }
1✔
169

170
    @Override
171
    public boolean equals(Object object) {
172
        return object instanceof Network && areNetworksEqual(this, (Network) object);
×
173
    }
174

175
    @Override
176
    public CompoundTag toNBT(HolderLookup.Provider provider) {
177
        CompoundTag tag = new CompoundTag();
4✔
178
        tag.put("baseCluster", this.baseCluster.toNBT(provider));
8✔
179
        tag.putBoolean("crashed", this.crashed);
5✔
180
        return tag;
2✔
181
    }
182

183
    @Override
184
    public void fromNBT(HolderLookup.Provider provider, CompoundTag tag) {
185
        // NBT reading is postponed until the first network tick, to ensure that the game is properly initialized.
186
        // Because other mods may register things such as dimensions at the same time when networks
187
        // are being constructed (as was the case in #349)
188
        this.toRead = tag;
×
189
        this.provider = provider;
×
190
    }
×
191

192
    public void fromNBTEffective(HolderLookup.Provider provider, CompoundTag tag) {
193
        this.baseCluster.fromNBT(provider, tag.getCompound("baseCluster"));
×
194
        this.crashed = tag.getBoolean("crashed");
×
195
        deriveNetworkElements(baseCluster);
×
196
        initialize(true);
×
197
    }
×
198

199
    @Override
200
    public synchronized boolean addNetworkElement(INetworkElement element, boolean networkPreinit) {
201
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
202
            if (!fullNetworkListener.addNetworkElement(element, networkPreinit)) {
5!
203
                return false;
×
204
            }
205
        }
206

207
        if(getEventBus().postCancelable(new NetworkElementAddEvent.Pre(this, element))) {
9!
208
            elements.add(element);
5✔
209
            if (!element.onNetworkAddition(this)) {
4!
210
                elements.remove(element);
×
211
                return false;
×
212
            }
213
            if (!networkPreinit) {
2✔
214
                addNetworkElementUpdateable(element);
3✔
215
            }
216
            if (element instanceof IEventListenableNetworkElement) {
3✔
217
                IEventListenableNetworkElement<?> listenableElement = (IEventListenableNetworkElement<?>) element;
3✔
218
                listenableElement.getNetworkEventListener().ifPresent(listener -> {
6✔
219
                    if (listener.hasEventSubscriptions()) {
3✔
220
                        for (Class<? extends INetworkEvent> eventType : listener.getSubscribedEvents()) {
11✔
221
                            getEventBus().register(listenableElement, eventType);
5✔
222
                        }
1✔
223
                    }
224
                });
1✔
225
            }
226
            getEventBus().post(new NetworkElementAddEvent.Post(this, element));
8✔
227
            onNetworkChanged();
2✔
228
            return true;
2✔
229
        }
230
        return false;
×
231
    }
232

233
    @Override
234
    public void addNetworkElementUpdateable(INetworkElement element) {
235
        if(element.isUpdate()) {
3✔
236
            updateableElementsTicks.put(element, 0);
6✔
237
        }
238
    }
1✔
239

240
    @Override
241
    public boolean removeNetworkElementPre(INetworkElement element) {
242
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
243
            if (!fullNetworkListener.removeNetworkElementPre(element)) {
4!
244
                return false;
×
245
            }
246
        }
247
        return getEventBus().postCancelable(new NetworkElementRemoveEvent.Pre(this, element));
9✔
248
    }
249

250
    @Override
251
    public synchronized void setPriorityAndChannel(INetworkElement element, int priority, int channel) {
252
        elements.remove(element);
×
253
        int oldTickValue = updateableElementsTicks.defaultReturnValue();
×
254
        if (element.isUpdate()) {
×
255
            oldTickValue = updateableElementsTicks.removeInt(element);
×
256
        }
257

258
        //noinspection deprecation
259
        element.setPriorityAndChannel(this, priority, channel);
×
260

261
        elements.add(element);
×
262
        if (element.isUpdate()) {
×
263
            updateableElementsTicks.put(
×
264
                element,
265
                oldTickValue == updateableElementsTicks.defaultReturnValue()
×
266
                    ? element.getUpdateInterval()
×
267
                    : oldTickValue
×
268
            );
269
        }
270
    }
×
271

272
    @Override
273
    public void removeNetworkElementPost(INetworkElement element, BlockState blockState) {
274
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
275
            fullNetworkListener.removeNetworkElementPost(element, blockState);
4✔
276
        }
277
        if (element instanceof IEventListenableNetworkElement) {
3✔
278
            IEventListenableNetworkElement<?> listenableElement = (IEventListenableNetworkElement<?>) element;
3✔
279
            listenableElement.getNetworkEventListener().ifPresent(listener -> {
6✔
280
                if (listener.hasEventSubscriptions()) {
3✔
281
                    getEventBus().unregister(listenableElement);
4✔
282
                }
283
            });
1✔
284
        }
285
        element.beforeNetworkKill(this, blockState);
4✔
286
        element.onNetworkRemoval(this, blockState);
4✔
287
        elements.remove(element);
5✔
288
        removeNetworkElementUpdateable(element);
3✔
289
        invalidatedElements.remove(element); // The element may be invalidated (like in an unloaded chunk) when it is being removed.
5✔
290
        getEventBus().post(new NetworkElementRemoveEvent.Post(this, element));
8✔
291
        onNetworkChanged();
2✔
292
    }
1✔
293

294
    @Override
295
    public synchronized void removeNetworkElementUpdateable(INetworkElement element) {
296
        if (isInitialized()) {
3!
297
            updateableElementsTicks.removeInt(element);
5✔
298
        }
299
    }
1✔
300

301
    /**
302
     * Called when a network is server-loaded or newly created.
303
     * @param silent If the element should not be notified for the network becoming alive.
304
     */
305
    protected void initialize(boolean silent) {
306
        updateableElementsTicks = new Object2IntAVLTreeMap<>();
5✔
307
        updateableElementsTicks.defaultReturnValue(Integer.MIN_VALUE);
4✔
308
        for(INetworkElement element : elements) {
11✔
309
            addNetworkElementUpdateable(element);
3✔
310
            if(!silent) {
2!
311
                element.afterNetworkAlive(this);
3✔
312
            }
313
            element.afterNetworkReAlive(this);
3✔
314
        }
1✔
315

316
        // Once all elements are alive, send a single variable contents updated event.
317
        this.getEventBus().post(new VariableContentsUpdatedEvent(this));
7✔
318
    }
1✔
319

320
    @Override
321
    public void kill() {
322
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
323
            fullNetworkListener.kill();
2✔
324
        }
325
        for(INetworkElement element : elements) {
7!
326
            element.beforeNetworkKill(this);
×
327
        }
×
328
        killed = true;
3✔
329
    }
1✔
330

331
    @Override
332
    public boolean killIfEmpty() {
333
        if(baseCluster.isEmpty()) {
4✔
334
            kill();
2✔
335
            onNetworkChanged();
2✔
336
            return true;
2✔
337
        }
338
        return false;
2✔
339
    }
340

341
    @Override
342
    public boolean canUpdate(INetworkElement element) {
343
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
344
            if (!fullNetworkListener.canUpdate(element)) {
4!
345
                return false;
×
346
            }
347
        }
348
        return true;
2✔
349
    }
350

351
    @Override
352
    public void postUpdate(INetworkElement element) {
353
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
354
            fullNetworkListener.postUpdate(element);
3✔
355
        }
356
    }
1✔
357

358
    @Override
359
    public void onSkipUpdate(INetworkElement element) {
360
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
×
361
            fullNetworkListener.onSkipUpdate(element);
×
362
        }
363
    }
×
364

365
    @Override
366
    public void updateGuaranteed() {
367
        if (this.toRead != null) {
3!
368
            this.fromNBTEffective(this.provider, this.toRead);
×
369
            this.toRead = null;
×
370
            this.provider = null;
×
371
        }
372
    }
1✔
373

374
    @Override
375
    public final synchronized void update() {
376
        this.changed = false;
3✔
377
        if(killIfEmpty() || killed) {
6!
378
            NetworkWorldStorage.getInstance(IntegratedDynamics._instance).removeInvalidatedNetwork(this);
5✔
379
        } else {
380
            onUpdate();
2✔
381

382
            // Update updateable network elements
383
            boolean isBeingDiagnozed = NetworkDiagnostics.getInstance().isBeingDiagnozed();
3✔
384
            if (!isBeingDiagnozed && !lastSecondDurations.isEmpty()) {
6!
385
                // Make sure we aren't using any unnecessary memory.
386
                lastSecondDurations.clear();
×
387
            }
388
            for (Object2IntMap.Entry<INetworkElement> entry : updateableElementsTicks.object2IntEntrySet()) {
12✔
389
                var element = entry.getKey();
4✔
390
                try {
391
                    if (isValid(element)) {
4!
392
                        long startTime = 0;
2✔
393
                        if (isBeingDiagnozed) {
2!
394
                            startTime = System.nanoTime();
×
395
                        }
396
                        int lastElementTick = entry.getIntValue();
3✔
397
                        if (canUpdate(element)) {
4!
398
                            if (lastElementTick <= 0) {
2!
399
                                entry.setValue(element.getUpdateInterval() - 1);
7✔
400
                                element.update(this);
3✔
401
                                postUpdate(element);
4✔
402
                            } else {
403
                                entry.setValue(lastElementTick - 1);
×
404
                            }
405
                        } else {
406
                            onSkipUpdate(element);
×
407
                            entry.setValue(lastElementTick - 1);
×
408
                        }
409
                        if (isBeingDiagnozed) {
2!
410
                            long duration = System.nanoTime() - startTime;
×
411
                            Long lastDuration = lastSecondDurations.get(element);
×
412
                            if (lastDuration != null) {
×
413
                                duration = duration + lastDuration;
×
414
                            }
415
                            lastSecondDurations.put(element, duration);
×
416
                        }
417
                    }
418
                } catch (PartStateException e) {
×
419
                    IntegratedDynamics.clog(org.apache.logging.log4j.Level.WARN, "Attempted to tick a part that was not properly unloaded. " +
×
420
                            "Report this to the Integrated Dynamics issue tracker with details on what you did " +
421
                            "leading up to this stacktrace. The part was forcefully unloaded");
422
                    e.printStackTrace();
×
423
                    element.invalidate(this);
×
424
                }
1✔
425
            }
1✔
426
        }
427
    }
1✔
428

429
    protected void onUpdate() {
430
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
431
            fullNetworkListener.update();
2✔
432
        }
433
    }
1✔
434

435
    @Override
436
    public synchronized boolean removePathElement(IPathElement pathElement, Direction side, BlockState blockState) {
437
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
438
            if (!fullNetworkListener.removePathElement(pathElement, side, blockState)) {
6!
439
                return false;
×
440
            }
441
        }
442
        if(baseCluster.remove(SidedPathElement.of(pathElement, null))) {
7!
443
            DimPos position = pathElement.getPosition();
3✔
444
            Level level = position.getLevel(true);
4✔
445
            INetworkElementProvider networkElementProvider = null;
2✔
446
            if (level != null) {
2!
447
                networkElementProvider = level.getCapability(Capabilities.NetworkElementProvider.BLOCK, position.getBlockPos(), blockState, null, side);
10✔
448
            }
449
            if (networkElementProvider != null) {
2!
450
                Collection<INetworkElement> networkElements = networkElementProvider.
3✔
451
                        createNetworkElements(position.getLevel(true), position.getBlockPos());
5✔
452
                for (INetworkElement networkElement : networkElements) {
10✔
453
                    if(!removeNetworkElementPre(networkElement)) {
4!
454
                        return false;
×
455
                    }
456
                }
1✔
457
                for (INetworkElement networkElement : networkElements) {
10✔
458
                    removeNetworkElementPost(networkElement, blockState);
4✔
459
                }
1✔
460
                onNetworkChanged();
2✔
461
                return true;
2✔
462
            }
463
        } else {
×
464
            Thread.dumpStack();
×
465
            IntegratedDynamics.clog(org.apache.logging.log4j.Level.WARN, "Tried to remove a path element from a network it was not present in.");
×
466
            System.out.println("Cluster: " + baseCluster);
×
467
            System.out.println("Tried removing element: " + pathElement);
×
468
        }
469
        return false;
×
470
    }
471

472
    @Override
473
    public void afterServerLoad() {
474
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
×
475
            fullNetworkListener.afterServerLoad();
×
476
        }
477
        // All networks start from an invalidated state at server start
478
        for (INetworkElement element : getElements()) {
×
479
            invalidateElement(element);
×
480
        }
×
481

482
    }
×
483

484
    @Override
485
    public void beforeServerStop() {
486
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
487
            fullNetworkListener.beforeServerStop();
2✔
488
        }
489
    }
1✔
490

491
    @Override
492
    public Set<INetworkElement> getElements() {
493
        return this.elements;
×
494
    }
495

496
    @Override
497
    public boolean isKilled() {
498
        return this.killed;
×
499
    }
500

501
    protected void onNetworkChanged() {
502
        this.changed = true;
3✔
503
    }
1✔
504

505
    @Override
506
    public boolean hasChanged() {
507
        return this.changed;
×
508
    }
509

510
    @Override
511
    public int getCablesCount() {
512
        return baseCluster.size();
×
513
    }
514

515
    @Override
516
    public long getLastSecondDuration(INetworkElement networkElement) {
517
        Long duration = lastSecondDurations.get(networkElement);
×
518
        return duration == null ? 0 : duration;
×
519
    }
520

521
    @Override
522
    public void resetLastSecondDurations() {
523
        lastSecondDurations.clear();
×
524
    }
×
525

526
    @Override
527
    public boolean isCrashed() {
528
        return crashed;
3✔
529
    }
530

531
    @Override
532
    public void setCrashed(boolean crashed) {
533
        this.crashed = crashed;
×
534
    }
×
535

536
    @Override
537
    public <T> Optional<T> getCapability(NetworkCapability<T> capability) {
538
        return Optional.ofNullable(capability.getCapability(this.capabilityProviders, this));
7✔
539
    }
540

541
    @Override
542
    public void invalidateElement(INetworkElement element) {
543
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
17✔
544
            fullNetworkListener.invalidateElement(element);
3✔
545
        }
546
        invalidatedElements.add(element);
5✔
547
    }
1✔
548

549
    @Override
550
    public void revalidateElement(INetworkElement element) {
551
        for (IFullNetworkListener fullNetworkListener : this.fullNetworkListeners) {
×
552
            fullNetworkListener.revalidateElement(element);
×
553
        }
554
        invalidatedElements.remove(element);
×
555
    }
×
556

557
    @Override
558
    public boolean containsSidedPathElement(ISidedPathElement pathElement) {
559
        return baseCluster.contains(pathElement);
×
560
    }
561

562
    @Override
563
    public IFullNetworkListener[] getFullNetworkListeners() {
564
        return this.fullNetworkListeners;
×
565
    }
566

567
    @Override
568
    public boolean isValid(INetworkElement element) {
569
        if (invalidatedElements.contains(element)) {
5!
570
            if (element.canRevalidate(this)) {
×
571
                element.revalidate(this);
×
572
                return true;
×
573
            }
574
            return false;
×
575
        }
576
        return true;
2✔
577
    }
578
}
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