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

CyclopsMC / IntegratedDynamics / 14549182042

19 Apr 2025 12:35PM UTC coverage: 42.579% (+0.004%) from 42.575%
14549182042

push

github

rubensworks
Fix wrench not removing cables after using off-hand item, Closes #1504

2310 of 8383 branches covered (27.56%)

Branch coverage included in aggregate %.

11021 of 22926 relevant lines covered (48.07%)

2.26 hits per line

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

65.02
/src/main/java/org/cyclops/integrateddynamics/block/BlockCable.java
1
package org.cyclops.integrateddynamics.block;
2

3
import com.google.common.cache.Cache;
4
import com.google.common.cache.CacheBuilder;
5
import com.mojang.serialization.MapCodec;
6
import lombok.Setter;
7
import lombok.SneakyThrows;
8
import net.minecraft.core.BlockPos;
9
import net.minecraft.core.Direction;
10
import net.minecraft.server.level.ServerLevel;
11
import net.minecraft.util.RandomSource;
12
import net.minecraft.world.InteractionHand;
13
import net.minecraft.world.InteractionResult;
14
import net.minecraft.world.entity.LivingEntity;
15
import net.minecraft.world.entity.player.Player;
16
import net.minecraft.world.item.ItemStack;
17
import net.minecraft.world.item.context.BlockPlaceContext;
18
import net.minecraft.world.level.*;
19
import net.minecraft.world.level.block.BaseEntityBlock;
20
import net.minecraft.world.level.block.Block;
21
import net.minecraft.world.level.block.RenderShape;
22
import net.minecraft.world.level.block.SimpleWaterloggedBlock;
23
import net.minecraft.world.level.block.entity.BlockEntity;
24
import net.minecraft.world.level.block.entity.BlockEntityTicker;
25
import net.minecraft.world.level.block.entity.BlockEntityType;
26
import net.minecraft.world.level.block.state.BlockState;
27
import net.minecraft.world.level.block.state.StateDefinition;
28
import net.minecraft.world.level.block.state.properties.BlockStateProperties;
29
import net.minecraft.world.level.block.state.properties.BooleanProperty;
30
import net.minecraft.world.level.material.Fluid;
31
import net.minecraft.world.level.material.FluidState;
32
import net.minecraft.world.level.material.Fluids;
33
import net.minecraft.world.phys.AABB;
34
import net.minecraft.world.phys.BlockHitResult;
35
import net.minecraft.world.phys.shapes.*;
36
import net.neoforged.neoforge.client.model.data.ModelProperty;
37
import net.neoforged.neoforge.common.extensions.ILevelExtension;
38
import org.cyclops.cyclopscore.block.BlockWithEntity;
39
import org.cyclops.cyclopscore.datastructure.EnumFacingMap;
40
import org.cyclops.cyclopscore.helper.BlockEntityHelpers;
41
import org.cyclops.integrateddynamics.Capabilities;
42
import org.cyclops.integrateddynamics.RegistryEntries;
43
import org.cyclops.integrateddynamics.api.block.IDynamicLight;
44
import org.cyclops.integrateddynamics.api.block.IDynamicRedstone;
45
import org.cyclops.integrateddynamics.api.block.cable.ICableFakeable;
46
import org.cyclops.integrateddynamics.api.part.IPartContainer;
47
import org.cyclops.integrateddynamics.api.part.IPartState;
48
import org.cyclops.integrateddynamics.api.part.IPartType;
49
import org.cyclops.integrateddynamics.api.part.PartRenderPosition;
50
import org.cyclops.integrateddynamics.block.shapes.*;
51
import org.cyclops.integrateddynamics.client.model.CableModel;
52
import org.cyclops.integrateddynamics.client.model.IRenderState;
53
import org.cyclops.integrateddynamics.core.block.BlockRayTraceResultComponent;
54
import org.cyclops.integrateddynamics.core.block.VoxelShapeComponents;
55
import org.cyclops.integrateddynamics.core.block.VoxelShapeComponentsFactory;
56
import org.cyclops.integrateddynamics.core.blockentity.BlockEntityMultipartTicking;
57
import org.cyclops.integrateddynamics.core.helper.CableHelpers;
58
import org.cyclops.integrateddynamics.core.helper.NetworkHelpers;
59
import org.cyclops.integrateddynamics.core.helper.PartHelpers;
60

61
import javax.annotation.Nullable;
62
import java.util.Collection;
63
import java.util.Iterator;
64
import java.util.Map;
65
import java.util.Optional;
66
import java.util.concurrent.TimeUnit;
67

68
/**
69
 * A block that is built up from different parts.
70
 * This block refers to a ticking part entity.
71
 * @author rubensworks
72
 */
73
public class BlockCable extends BlockWithEntity implements SimpleWaterloggedBlock {
74

75
    public static final MapCodec<BlockCable> CODEC = simpleCodec(BlockCable::new);
3✔
76

77
    public static final float BLOCK_HARDNESS = 3.0F;
78

79
    public static final BooleanProperty WATERLOGGED = BlockStateProperties.WATERLOGGED;
2✔
80

81
    // Model Properties
82
    public static final ModelProperty<Boolean> REALCABLE = new ModelProperty<>();
4✔
83
    public static final ModelProperty<Boolean>[] CONNECTED = new ModelProperty[6];
3✔
84
    public static final ModelProperty<PartRenderPosition>[] PART_RENDERPOSITIONS = new ModelProperty[6];
3✔
85
    public static final ModelProperty<Optional<BlockState>> FACADE = new ModelProperty<>();
4✔
86
    static {
87
        for(Direction side : Direction.values()) {
16✔
88
            CONNECTED[side.ordinal()] = new ModelProperty<>();
7✔
89
            PART_RENDERPOSITIONS[side.ordinal()] = new ModelProperty<>();
7✔
90
        }
91
    }
92
    public static final ModelProperty<IPartContainer> PARTCONTAINER = new ModelProperty<>();
4✔
93
    public static final ModelProperty<IRenderState> RENDERSTATE = new ModelProperty<>();
4✔
94

95
    // Collision boxes
96
    public final static AABB CABLE_CENTER_BOUNDINGBOX = new AABB(
10✔
97
            CableModel.MIN, CableModel.MIN, CableModel.MIN, CableModel.MAX, CableModel.MAX, CableModel.MAX);
98
    private final static EnumFacingMap<AABB> CABLE_SIDE_BOUNDINGBOXES = EnumFacingMap.forAllValues(
57✔
99
            new AABB(CableModel.MIN, 0, CableModel.MIN, CableModel.MAX, CableModel.MIN, CableModel.MAX), // DOWN
100
            new AABB(CableModel.MIN, CableModel.MAX, CableModel.MIN, CableModel.MAX, 1, CableModel.MAX), // UP
101
            new AABB(CableModel.MIN, CableModel.MIN, 0, CableModel.MAX, CableModel.MAX, CableModel.MIN), // NORTH
102
            new AABB(CableModel.MIN, CableModel.MAX, CableModel.MAX, CableModel.MAX, CableModel.MIN, 1), // SOUTH
103
            new AABB(0, CableModel.MIN, CableModel.MIN, CableModel.MIN, CableModel.MAX, CableModel.MAX), // WEST
104
            new AABB(CableModel.MAX, CableModel.MIN, CableModel.MIN, 1, CableModel.MAX, CableModel.MAX) // EAST
105
    );
106

107
    private final VoxelShapeComponentsFactory voxelShapeComponentsFactory = new VoxelShapeComponentsFactory(
31✔
108
            new VoxelShapeComponentsFactoryHandlerCableCenter(),
109
            new VoxelShapeComponentsFactoryHandlerCableConnections(),
110
            new VoxelShapeComponentsFactoryHandlerParts(),
111
            new VoxelShapeComponentsFactoryHandlerFacade()
112
    );
113

114
    @Setter
7✔
115
    private boolean disableCollisionBox = false;
116

117
    public BlockCable(Properties properties) {
118
        super(properties, BlockEntityMultipartTicking::new);
4✔
119
        this.registerDefaultState(this.stateDefinition.any().setValue(WATERLOGGED, false));
11✔
120
    }
1✔
121

122
    @Override
123
    protected MapCodec<? extends BaseEntityBlock> codec() {
124
        return CODEC;
×
125
    }
126

127
    @Override
128
    public boolean useShapeForLightOcclusion(BlockState p_60576_) {
129
        return true;
2✔
130
    }
131

132
    @Override
133
    @Nullable
134
    public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState blockState, BlockEntityType<T> blockEntityType) {
135
        return level.isClientSide ? null : createTickerHelper(blockEntityType, RegistryEntries.BLOCK_ENTITY_MULTIPART_TICKING.get(), new BlockEntityMultipartTicking.Ticker<>());
12!
136
    }
137

138
    @Override
139
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
140
        super.createBlockStateDefinition(builder);
3✔
141
        builder.add(WATERLOGGED);
9✔
142
    }
1✔
143

144
    @Override
145
    public BlockState updateShape(BlockState stateIn, Direction facing, BlockState facingState, LevelAccessor worldIn, BlockPos currentPos, BlockPos facingPos) {
146
        if (stateIn.getValue(WATERLOGGED)) {
6!
147
            worldIn.scheduleTick(currentPos, Fluids.WATER, Fluids.WATER.getTickDelay(worldIn));
×
148
        }
149
        NetworkHelpers.onElementProviderBlockNeighborChange((Level) worldIn, currentPos, facingState.getBlock(), facing, facingPos);
8✔
150
        return super.updateShape(stateIn, facing, facingState, worldIn, currentPos, facingPos);
9✔
151
    }
152

153
    @Override
154
    public BlockState getStateForPlacement(BlockPlaceContext context) {
155
        FluidState ifluidstate = context.getLevel().getFluidState(context.getClickedPos());
6✔
156
        return this.defaultBlockState().setValue(WATERLOGGED, ifluidstate.getType() == Fluids.WATER);
12!
157
    }
158

159
    @Override
160
    public FluidState getFluidState(BlockState state) {
161
        return state.getValue(WATERLOGGED) ? Fluids.WATER.getSource(false) : super.getFluidState(state);
14✔
162
    }
163

164
    @Override
165
    public boolean canPlaceLiquid(@org.jetbrains.annotations.Nullable Player player, BlockGetter worldIn, BlockPos pos, BlockState blockState, Fluid fluidIn) {
166
        return !blockState.getValue(BlockStateProperties.WATERLOGGED) && fluidIn == Fluids.WATER
×
167
                && !(worldIn instanceof ILevelExtension levelExtension && CableHelpers.hasFacade(levelExtension, pos));
×
168
    }
169

170
    @Override
171
    public void onBlockExploded(BlockState state, Level world, BlockPos blockPos, Explosion explosion) {
172
        CableHelpers.setRemovingCable(true);
2✔
173
        CableHelpers.onCableRemoving(world, blockPos, true, false, state);
7✔
174
        Collection<Direction> connectedCables = CableHelpers.getExternallyConnectedCables(world, blockPos);
4✔
175
        super.onBlockExploded(state, world, blockPos, explosion);
6✔
176
        CableHelpers.onCableRemoved(world, blockPos, connectedCables);
5✔
177
        CableHelpers.setRemovingCable(false);
2✔
178
    }
1✔
179

180
    @Override
181
    public boolean onDestroyedByPlayer(BlockState state, Level world, BlockPos pos, Player player, boolean willHarvest, FluidState fluid) {
182
        BlockRayTraceResultComponent rayTraceResult = getSelectedShape(state, world, pos, CollisionContext.of(player))
9✔
183
                .rayTrace(pos, player);
2✔
184
        if (rayTraceResult != null && rayTraceResult.getComponent().destroy(world, pos, player, false)) {
10!
185
            return false;
2✔
186
        }
187
        return rayTraceResult != null && super.onDestroyedByPlayer(state, world, pos, player, willHarvest, fluid);
13!
188
    }
189

190
    @Override
191
    public void onRemove(BlockState state, Level world, BlockPos blockPos, BlockState newState, boolean isMoving) {
192
        if (newState.getBlock() != this) {
4!
193
            Collection<Direction> connectedCables = null;
2✔
194
            if (!CableHelpers.isRemovingCable()) {
2✔
195
                CableHelpers.onCableRemoving(world, blockPos, false, false, state);
7✔
196
                connectedCables = CableHelpers.getExternallyConnectedCables(world, blockPos);
4✔
197
            }
198
            super.onRemove(state, world, blockPos, newState, isMoving);
7✔
199
            if (!CableHelpers.isRemovingCable()) {
2✔
200
                CableHelpers.onCableRemoved(world, blockPos, connectedCables);
5✔
201
            }
202
        } else {
1✔
203
            super.onRemove(state, world, blockPos, newState, isMoving);
×
204
        }
205
    }
1✔
206

207
    @Override
208
    public InteractionResult useWithoutItem(BlockState state, Level world, BlockPos pos, Player player, BlockHitResult hit) {
209
        /*
210
            Wrench: sneak + right-click anywhere on cable to remove cable
211
                    right-click on a cable side to disconnect on that side
212
                    sneak + right-click on part to remove that part
213
            No wrench: right-click to open GUI
214
         */
215
        BlockEntityMultipartTicking tile = BlockEntityHelpers.get(world, pos, BlockEntityMultipartTicking.class).orElse(null);
8✔
216
        if(tile != null) {
2!
217
            BlockRayTraceResultComponent rayTraceResult = getSelectedShape(state, world, pos, CollisionContext.of(player))
9✔
218
                    .rayTrace(pos, player);
2✔
219
            if(rayTraceResult != null) {
2!
220
                InteractionResult actionResultType = rayTraceResult.getComponent().onBlockActivated(state, world, pos, player, InteractionHand.MAIN_HAND, rayTraceResult);
10✔
221
                if (actionResultType.consumesAction()) {
3!
222
                    return actionResultType;
2✔
223
                }
224
            }
225
        }
226
        return super.useWithoutItem(state, world, pos, player, hit);
×
227
    }
228

229
    @Override
230
    public void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean isMoving) {
231
        super.onPlace(state, world, pos, oldState, isMoving);
7✔
232
        if (!world.isClientSide()) {
3!
233
            ICableFakeable cableFakeable = CableHelpers.getCableFakeable(world, pos, null).orElse(null);
8✔
234
            if (cableFakeable != null && cableFakeable.isRealCable()) {
5!
235
                CableHelpers.onCableAdded(world, pos);
3✔
236
            }
237
        }
238
    }
1✔
239

240
    @Override
241
    public void setPlacedBy(Level world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) {
242
        super.setPlacedBy(world, pos, state, placer, itemStack);
7✔
243
        if (!world.isClientSide()) {
3!
244
            CableHelpers.onCableAddedByPlayer(world, pos, placer);
4✔
245
        }
246
    }
1✔
247

248
    @Override
249
    public ItemStack getCloneItemStack(BlockState state, net.minecraft.world.phys.HitResult target, LevelReader world,
250
                                  BlockPos blockPos, Player player) {
251
        BlockRayTraceResultComponent rayTraceResult = getSelectedShape(state, world, blockPos, CollisionContext.of(player))
×
252
                .rayTrace(blockPos, player);
×
253
        if(rayTraceResult != null) {
×
254
            return rayTraceResult.getComponent().getCloneItemStack((Level) world, blockPos);
×
255
        }
256
        return getCloneItemStack(world, blockPos, state);
×
257
    }
258

259
    @SuppressWarnings("deprecation")
260
    @Override
261
    public void neighborChanged(BlockState state, Level world, BlockPos pos, Block neighborBlock, BlockPos fromPos, boolean isMoving) {
262
        super.neighborChanged(state, world, pos, neighborBlock, fromPos, isMoving);
8✔
263
        NetworkHelpers.onElementProviderBlockNeighborChange(world, pos, neighborBlock, null, fromPos);
6✔
264
    }
1✔
265

266
    @Override
267
    public void onNeighborChange(BlockState state, LevelReader world, BlockPos pos, BlockPos neighbor) {
268
        super.onNeighborChange(state, world, pos, neighbor);
6✔
269
        if (world instanceof Level level) {
6!
270
            NetworkHelpers.onElementProviderBlockNeighborChange(level, pos, world.getBlockState(neighbor).getBlock(), null, neighbor);
9✔
271
        }
272
    }
1✔
273

274
    @Override
275
    public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource rand) {
276
        super.tick(state, world, pos, rand);
×
277
        BlockEntityHelpers.get(world, pos, BlockEntityMultipartTicking.class)
×
278
                .ifPresent(tile -> {
×
279
                    for (Map.Entry<Direction, PartHelpers.PartStateHolder<?, ?>> entry : tile
×
280
                            .getPartContainer().getPartData().entrySet()) {
×
281
                        updateTickPart(entry.getValue().getPart(), world, pos, entry.getValue().getState(), rand);
×
282
                    }
×
283
                });
×
284
    }
×
285

286
    protected void updateTickPart(IPartType partType, Level world, BlockPos pos, IPartState partState, RandomSource random) {
287
        partType.updateTick(world, pos, partState, random);
×
288
    }
×
289

290
    /* --------------- Start shapes and rendering --------------- */
291

292
    public AABB getCableBoundingBox(Direction side) {
293
        if (side == null) {
×
294
            return CABLE_CENTER_BOUNDINGBOX;
×
295
        } else {
296
            return CABLE_SIDE_BOUNDINGBOXES.get(side);
×
297
        }
298
    }
299

300
    public VoxelShapeComponents getSelectedShape(BlockState blockState, BlockGetter world, BlockPos pos, CollisionContext selectionContext) {
301
        return voxelShapeComponentsFactory.createShape(blockState, world, pos, selectionContext);
8✔
302
    }
303

304
    private final Cache<String, VoxelShape> CACHE_COLLISION_SHAPES = CacheBuilder.newBuilder()
4✔
305
            .expireAfterAccess(1, TimeUnit.MINUTES)
1✔
306
            .build();
2✔
307

308
    @SneakyThrows
×
309
    @Override
310
    public VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext selectionContext) {
311
        VoxelShapeComponents selectedShape = getSelectedShape(state, world, pos, selectionContext);
7✔
312
        BlockRayTraceResultComponent rayTraceResult = selectedShape.rayTrace(pos, selectionContext instanceof EntityCollisionContext ? ((EntityCollisionContext) selectionContext).getEntity() : null);
11!
313
        if (rayTraceResult != null) {
2!
314
            return rayTraceResult.getComponent().getShape(state, world, pos, selectionContext);
×
315
        }
316

317
        String cableState = selectedShape.getStateId();
3✔
318

319
        // Cache the operations below, as they are too expensive to execute each render tick
320
        return CACHE_COLLISION_SHAPES.get(cableState, () -> {
8✔
321
            // Combine all VoxelShapes using IBooleanFunction.OR,
322
            // because for some reason our VoxelShapeComponents aggregator does not handle collisions properly.
323
            // This can probably be fixed, but I spent too much time on this already, and the current solution works just fine.
324
            Iterator<VoxelShape> it = selectedShape.iterator();
3✔
325
            if (!it.hasNext()) {
3✔
326
                return Shapes.empty();
2✔
327
            }
328
            VoxelShape shape = it.next();
4✔
329
            while (it.hasNext()) {
3✔
330
                shape = Shapes.join(shape, it.next(), BooleanOp.OR);
8✔
331
            }
332
            return shape.optimize();
3✔
333
        });
334
    }
335

336
    @Override
337
    public VoxelShape getCollisionShape(BlockState blockState, BlockGetter world, BlockPos pos, CollisionContext selectionContext) {
338
        return disableCollisionBox ? Shapes.empty() : super.getCollisionShape(blockState, world, pos, selectionContext);
10!
339
    }
340

341
    @Override
342
    public boolean hasDynamicShape() {
343
        return BlockCableConfig.dynamicShape;
2✔
344
    }
345

346
    @Override
347
    public int getLightBlock(BlockState blockState, BlockGetter world, BlockPos pos) {
348
        if (world instanceof Level level) {
6✔
349
            if (CableHelpers.isLightTransparent(level, pos, null, blockState)) {
6!
350
                return 0;
×
351
            }
352
            return CableHelpers.getFacade(level, pos, blockState)
8✔
353
                    .map(facade -> facade.getLightBlock(world, pos))
2✔
354
                    .orElse(0);
4✔
355
        }
356
        return 0;
2✔
357
    }
358

359
    @Override
360
    public RenderShape getRenderShape(BlockState blockState) {
361
        return RenderShape.MODEL;
×
362
    }
363

364
    @Override
365
    protected VoxelShape getBlockSupportShape(BlockState pState, BlockGetter pLevel, BlockPos pPos) {
366
        return this.getShape(pState, pLevel, pPos, new CollisionContextBlockSupport());
9✔
367
    }
368

369
    @Override
370
    public boolean shouldDisplayFluidOverlay(BlockState state, BlockAndTintGetter world, BlockPos pos, FluidState fluidState) {
371
        return world instanceof ILevelExtension levelExtension && CableHelpers.getFacade(levelExtension, pos).isPresent();
×
372
    }
373

374
    /* --------------- Start IDynamicRedstone --------------- */
375

376
    @SuppressWarnings("deprecation")
377
    @Override
378
    public boolean isSignalSource(BlockState blockState) {
379
        return true;
×
380
    }
381

382
    @Override
383
    public boolean canConnectRedstone(BlockState blockState, BlockGetter world, BlockPos pos, Direction side) {
384
        if (world instanceof ILevelExtension levelExtension) {
6!
385
            if (side == null) {
2!
386
                for (Direction dummySide : Direction.values()) {
×
387
                    IDynamicRedstone dynamicRedstone = BlockEntityHelpers.getCapability(levelExtension, pos, dummySide, Capabilities.DynamicRedstone.BLOCK).orElse(null);
×
388
                    if (dynamicRedstone != null && (dynamicRedstone.getRedstoneLevel() >= 0 || dynamicRedstone.isAllowRedstoneInput())) {
×
389
                        return true;
×
390
                    }
391
                }
392
                return false;
×
393
            }
394
            IDynamicRedstone dynamicRedstone = BlockEntityHelpers.getCapability(levelExtension, pos, side.getOpposite(), Capabilities.DynamicRedstone.BLOCK).orElse(null);
10✔
395
            return dynamicRedstone != null && (dynamicRedstone.getRedstoneLevel() >= 0 || dynamicRedstone.isAllowRedstoneInput());
12!
396
        }
397
        return false;
×
398
    }
399

400
    @SuppressWarnings("deprecation")
401
    @Override
402
    public int getDirectSignal(BlockState blockState, BlockGetter world, BlockPos pos, Direction side) {
403
        if (world instanceof ILevelExtension levelExtension) {
6!
404
            IDynamicRedstone dynamicRedstone = BlockEntityHelpers.getCapability(levelExtension, pos, side.getOpposite(), Capabilities.DynamicRedstone.BLOCK).orElse(null);
10✔
405
            return dynamicRedstone != null && dynamicRedstone.isDirect() ? dynamicRedstone.getRedstoneLevel() : 0;
7!
406
        }
407
        return 0;
×
408
    }
409

410
    @SuppressWarnings("deprecation")
411
    @Override
412
    public int getSignal(BlockState blockState, BlockGetter world, BlockPos pos, Direction side) {
413
        if (world instanceof ILevelExtension levelExtension) {
6!
414
            IDynamicRedstone dynamicRedstone = BlockEntityHelpers.getCapability(levelExtension, pos, side.getOpposite(), Capabilities.DynamicRedstone.BLOCK).orElse(null);
10✔
415
            return dynamicRedstone != null ? dynamicRedstone.getRedstoneLevel() : 0;
6!
416
        }
417
        return 0;
×
418
    }
419

420
    /* --------------- Start IDynamicLight --------------- */
421

422
    @Override
423
    public int getLightEmission(BlockState blockState, BlockGetter world, BlockPos pos) {
424
        int light = 0;
2✔
425
        if (world instanceof ILevelExtension levelExtension) {
6✔
426
            for (Direction side : Direction.values()) {
16✔
427
                IDynamicLight dynamicLight = levelExtension.getCapability(Capabilities.DynamicLight.BLOCK, pos, blockState, null, side);
9✔
428
                if (dynamicLight != null) {
2✔
429
                    light = Math.max(light, dynamicLight.getLightLevel());
5✔
430
                }
431
            }
432
        }
433
        return light;
2✔
434
    }
435

436
}
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