Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions src/main/java/org/eolang/lints/ThrottledLint.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import com.jcabi.xml.XML;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
* Throttled lint decorator.
* <p>
* Wraps a lint and limits its execution time to 10 seconds.
* If the lint takes longer than the timeout, it is interrupted
* and a single timeout defect is returned.
* </p>
* @since 0.0.47
*/
public final class ThrottledLint implements Lint {

/**
* Timeout in seconds.
*/
private static final int TIMEOUT = 10;

/**
* The lint to throttle.
*/
private final Lint decorated;

/**
* Ctor.
* @param lnt The lint to decorate
*/
public ThrottledLint(final Lint lnt) {
this.decorated = lnt;
}

@Override
public String name() {
return this.decorated.name();
}

@Override
public Collection<Defect> defects(final XML xmir) throws IOException {
final ExecutorService executor = Executors.newSingleThreadExecutor();
try {
final Future<Collection<Defect>> future = executor.submit(
new Callable<Collection<Defect>>() {
@Override
public Collection<Defect> call() {
try {
return ThrottledLint.this.decorated.defects(xmir);
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
}
}
);
try {
return future.get(ThrottledLint.TIMEOUT, TimeUnit.SECONDS);
} catch (final TimeoutException ex) {
future.cancel(true);
return Collections.singletonList(
new Defect.Default(
this.name(),
Severity.WARNING,
0,
String.format(
"Lint '%s' exceeded %d second timeout",
this.name(),
ThrottledLint.TIMEOUT
)
)
);
} catch (final ExecutionException ex) {
final Throwable cause = ex.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
}
throw new IOException(
String.format(
"Lint '%s' failed with error: %s",
this.name(),
cause.getMessage()
),
cause
);
} catch (final InterruptedException ex) {
Thread.currentThread().interrupt();
throw new IOException(
String.format(
"Lint '%s' was interrupted",
this.name()
),
ex
);
}
} finally {
executor.shutdownNow();
}
}

@Override
public String motive() throws IOException {
return this.decorated.motive();
}

@Override
public Fix fix() {
return this.decorated.fix();
}
}
133 changes: 133 additions & 0 deletions src/test/java/org/eolang/lints/ThrottledLintTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
* SPDX-License-Identifier: MIT
*/
package org.eolang.lints;

import com.jcabi.xml.XML;
import fixtures.EoProgram;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import org.hamcrest.MatcherAssert;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;

/**
* Test for {@link ThrottledLint}.
* @since 0.0.47
*/
final class ThrottledLintTest {

@Test
void forwardsName() {
MatcherAssert.assertThat(
"ThrottledLint should forward the wrapped lint's name",
new ThrottledLint(new LtAlways()).name(),
Matchers.equalTo("always")
);
}

@Test
void returnsDefectsFromWrappedLint() throws IOException {
final Collection<Defect> defects = new ThrottledLint(new LtAlways()).defects(
new EoProgram("org/eolang/lints/foo-without-dot.eo").parse()
);
MatcherAssert.assertThat(
"ThrottledLint should return defects from the wrapped lint",
defects,
Matchers.hasSize(1)
);
}

@Test
void returnsTimeoutDefectWhenLintTakesTooLong() throws IOException {
final Lint slow = new Lint() {
@Override
public String name() {
return "slow-lint";
}

@Override
public Collection<Defect> defects(final XML xmir) throws IOException {
try {
Thread.sleep(20_000);
} catch (final InterruptedException ex) {
Thread.currentThread().interrupt();
}
return Collections.emptyList();
}

@Override
public String motive() throws IOException {
return "A slow lint for testing";
}

@Override
public Fix fix() {
return new FxEmpty();
}
};
final Collection<Defect> defects = new ThrottledLint(slow).defects(
new EoProgram("org/eolang/lints/foo-without-dot.eo").parse()
);
MatcherAssert.assertThat(
"ThrottledLint should return a single timeout defect when lint exceeds 10s",
defects,
Matchers.hasSize(1)
);
final Defect defect = defects.iterator().next();
MatcherAssert.assertThat(
"Timeout defect should have WARNING severity",
defect.severity(),
Matchers.equalTo(Severity.WARNING)
);
MatcherAssert.assertThat(
"Timeout defect should mention the timeout",
defect.text(),
Matchers.containsString("timeout")
);
}

@Test
void returnsEmptyWhenWrappedLintReturnsEmpty() throws IOException {
final Lint empty = new Lint() {
@Override
public String name() {
return "empty-lint";
}

@Override
public Collection<Defect> defects(final XML xmir) throws IOException {
return Collections.emptyList();
}

@Override
public String motive() throws IOException {
return "A lint that returns nothing";
}

@Override
public Fix fix() {
return new FxEmpty();
}
};
final Collection<Defect> defects = new ThrottledLint(empty).defects(
new EoProgram("org/eolang/lints/foo-without-dot.eo").parse()
);
MatcherAssert.assertThat(
"ThrottledLint should return empty when wrapped lint returns empty",
defects,
Matchers.hasSize(0)
);
}

@Test
void delegatesFix() {
MatcherAssert.assertThat(
"ThrottledLint should delegate fix() to wrapped lint",
new ThrottledLint(new LtAlways()).fix(),
Matchers.notNullValue()
);
}
}
Loading