Merge remote-tracking branch 'upstream/master' into Error-handling
This commit is contained in:
commit
8f9719acbf
|
|
@ -21,4 +21,4 @@ jobs:
|
|||
- name: Run linter
|
||||
run: sbt "scalafixAll --check"
|
||||
- name: Run locust tests
|
||||
run: ./tests/run.sh
|
||||
run: sudo apt-get install -y tesseract-ocr && ./tests/run.sh
|
||||
|
|
|
|||
14
README.md
14
README.md
|
|
@ -22,7 +22,9 @@ The sample CAPTCHAs are also just that, samples. They have not been tested again
|
|||
## Quick start with Java
|
||||
|
||||
1. Download the `jar` file from the latest release
|
||||
2. Type `java -jar LibreCaptch.jar`
|
||||
2. Type `mkdir data/`.
|
||||
(The data directory is used to store a config file that you can tweak, and for storing the Database)
|
||||
2. Type `java -jar LibreCaptcha.jar`
|
||||
|
||||
We recommend a Java 11+ runtime as that's what we compile the code with.
|
||||
|
||||
|
|
@ -48,6 +50,7 @@ docker run -v lcdata:/lc-core/data librecaptcha/lc-core:latest
|
|||
|
||||
A default `config.json` is automatically created in the mounted volume.
|
||||
|
||||
## Quick test
|
||||
To test the installation, try:
|
||||
|
||||
```
|
||||
|
|
@ -60,7 +63,7 @@ To test the installation, try:
|
|||
sample.png: PNG image data, 350 x 100, 8-bit/color RGB, non-interlaced
|
||||
```
|
||||
|
||||
The API endpoints are described below.
|
||||
The API endpoints are described at the end of this file.
|
||||
|
||||
## Configuration
|
||||
If a `config.json` file is not present in the `data/` folder, the app creates one, and this can be modified
|
||||
|
|
@ -98,7 +101,12 @@ An image of a random string of alphabets is created. Then a series of image filt
|
|||
An image of a word is blurred before being shown to the user.
|
||||
|
||||
### LabelCaptcha
|
||||
An image that has a pair of words is created. The answer to one of the words is known and to that of the other is unknown. The user is tested on the known word, and their answer to the unknown word is recorded. If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words.
|
||||
This providers takes in two sets of images. One with known labels, and the other unknown.
|
||||
The created image has a pair of words one from each set.
|
||||
The user is tested on the known word, and their answer to the unknown word is recorded.
|
||||
If a sufficient number of users agree on their answer to the unknown word, it is transferred to the list of known words.
|
||||
|
||||
(There is a known issue with this provider; see issue #68 )
|
||||
|
||||
***
|
||||
|
||||
|
|
|
|||
|
|
@ -1,27 +1,38 @@
|
|||
import http.client
|
||||
import json
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
tempDir = os.getenv('XDG_RUNTIME_DIR', '.')
|
||||
conn = http.client.HTTPConnection('localhost', 8888)
|
||||
conn.request("GET", "/v1/token?email=test")
|
||||
response = conn.getresponse()
|
||||
responseStr = response.read()
|
||||
user = json.loads(responseStr)
|
||||
token = user["token"]
|
||||
|
||||
params = """{
|
||||
"level": "medium",
|
||||
"level": "debug",
|
||||
"media": "image/png",
|
||||
"input_type": "text"
|
||||
}"""
|
||||
|
||||
def getCaptcha():
|
||||
conn.request("POST", "/v1/captcha", body=params, headers={'access-token': user["token"]})
|
||||
conn.request("POST", "/v1/captcha", body=params)
|
||||
response = conn.getresponse()
|
||||
|
||||
if response:
|
||||
responseStr = response.read()
|
||||
return json.loads(responseStr)
|
||||
|
||||
def getAndSolve(idStr):
|
||||
conn.request("GET", "/v1/media?id=" + idStr)
|
||||
response = conn.getresponse()
|
||||
|
||||
if response:
|
||||
responseBytes = response.read()
|
||||
fileName = tempDir + "/captcha.png"
|
||||
with open(fileName, "wb") as f:
|
||||
f.write(responseBytes)
|
||||
ocrResult = subprocess.Popen("gocr " + fileName, shell=True, stdout=subprocess.PIPE)
|
||||
ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode()
|
||||
return ocrAnswer
|
||||
|
||||
def postAnswer(captchaId, ans):
|
||||
reply = {"answer": ans, "id" : captchaId}
|
||||
conn.request("POST", "/v1/answer", json.dumps(reply))
|
||||
|
|
@ -33,6 +44,6 @@ def postAnswer(captchaId, ans):
|
|||
|
||||
for i in range(0, 10000):
|
||||
captcha = getCaptcha()
|
||||
#print(captcha)
|
||||
captchaId = captcha["id"]
|
||||
print(i, postAnswer(captchaId, "xyz"))
|
||||
ans = getAndSolve(captchaId)
|
||||
print(i, postAnswer(captchaId, ans))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.awt.image.BufferedImage;
|
|||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
import lc.captchas.interfaces.Challenge;
|
||||
import lc.captchas.interfaces.ChallengeProvider;
|
||||
|
|
@ -18,13 +18,12 @@ public class FontFunCaptcha implements ChallengeProvider {
|
|||
return "FontFunCaptcha";
|
||||
}
|
||||
|
||||
public HashMap<String, List<String>> supportedParameters() {
|
||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
||||
supportedParams.put("supportedLevels", List.of("medium"));
|
||||
supportedParams.put("supportedMedia", List.of("image/png"));
|
||||
supportedParams.put("supportedInputType", List.of("text"));
|
||||
|
||||
return supportedParams;
|
||||
public Map<String, List<String>> supportedParameters() {
|
||||
return Map.of(
|
||||
"supportedLevels", List.of("medium"),
|
||||
"supportedMedia", List.of("image/png"),
|
||||
"supportedInputType", List.of("text")
|
||||
);
|
||||
}
|
||||
|
||||
public void configure(String config) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import java.awt.RenderingHints;
|
|||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
|
|
@ -55,13 +55,12 @@ public class GifCaptcha implements ChallengeProvider {
|
|||
// TODO: Add custom config
|
||||
}
|
||||
|
||||
public HashMap<String, List<String>> supportedParameters() {
|
||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
||||
supportedParams.put("supportedLevels", List.of("hard"));
|
||||
supportedParams.put("supportedMedia", List.of("image/gif"));
|
||||
supportedParams.put("supportedInputType", List.of("text"));
|
||||
|
||||
return supportedParams;
|
||||
public Map<String, List<String>> supportedParameters() {
|
||||
return Map.of(
|
||||
"supportedLevels", List.of("hard"),
|
||||
"supportedMedia", List.of("image/gif"),
|
||||
"supportedInputType", List.of("text")
|
||||
);
|
||||
}
|
||||
|
||||
public Challenge returnChallenge() {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import java.awt.image.BufferedImage;
|
|||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.List;
|
||||
|
||||
import lc.misc.HelperFunctions;
|
||||
|
|
@ -27,13 +27,12 @@ public class ShadowTextCaptcha implements ChallengeProvider {
|
|||
// TODO: Add custom config
|
||||
}
|
||||
|
||||
public HashMap<String, List<String>> supportedParameters() {
|
||||
HashMap<String, List<String>> supportedParams = new HashMap<String, List<String>>();
|
||||
supportedParams.put("supportedLevels", List.of("easy"));
|
||||
supportedParams.put("supportedMedia", List.of("image/png"));
|
||||
supportedParams.put("supportedInputType", List.of("text"));
|
||||
|
||||
return supportedParams;
|
||||
public Map<String, List<String>> supportedParameters() {
|
||||
return Map.of(
|
||||
"supportedLevels", List.of("easy"),
|
||||
"supportedMedia", List.of("image/png"),
|
||||
"supportedInputType", List.of("text")
|
||||
);
|
||||
}
|
||||
|
||||
public boolean checkAnswer(String secret, String answer) {
|
||||
|
|
|
|||
|
|
@ -11,9 +11,19 @@ public class HelperFunctions {
|
|||
RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
|
||||
}
|
||||
|
||||
public static String randomString(int n) {
|
||||
String characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz23456789$#%@&?";
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
public static final String safeAlphabets = "ABCDEFGHJKMNOPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
public static final String allAlphabets = safeAlphabets + "ILl";
|
||||
public static final String safeNumbers = "23456789";
|
||||
public static final String allNumbers = safeNumbers + "1";
|
||||
public static final String specialCharacters = "$#%@&?";
|
||||
public static final String safeCharacters = safeAlphabets + safeNumbers + specialCharacters;
|
||||
|
||||
public static String randomString(final int n) {
|
||||
return randomString(n, safeCharacters);
|
||||
}
|
||||
|
||||
public static String randomString(final int n, final String characters) {
|
||||
final StringBuilder stringBuilder = new StringBuilder();
|
||||
for (int i = 0; i < n; i++) {
|
||||
int index = (int) (characters.length() * Math.random());
|
||||
stringBuilder.append(characters.charAt(index));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
package lc.captchas
|
||||
|
||||
import javax.imageio.ImageIO
|
||||
import java.awt.Color
|
||||
import java.awt.Font
|
||||
import java.awt.font.TextLayout
|
||||
import java.awt.image.BufferedImage
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.Map
|
||||
import java.util.List
|
||||
|
||||
import lc.misc.HelperFunctions
|
||||
import lc.captchas.interfaces.Challenge
|
||||
import lc.captchas.interfaces.ChallengeProvider
|
||||
|
||||
/** This captcha is only for debugging purposes. It creates very simple captchas that are deliberately easy to solve with OCR engines */
|
||||
class DebugCaptcha extends ChallengeProvider {
|
||||
|
||||
def getId(): String = {
|
||||
"DebugCaptcha"
|
||||
}
|
||||
|
||||
def configure(config: String): Unit = {
|
||||
// TODO: Add custom config
|
||||
}
|
||||
|
||||
def supportedParameters(): Map[String, List[String]] = {
|
||||
Map.of(
|
||||
"supportedLevels", List.of("debug"),
|
||||
"supportedMedia", List.of("image/png"),
|
||||
"supportedInputType", List.of("text")
|
||||
)
|
||||
}
|
||||
|
||||
def checkAnswer(secret: String, answer: String): Boolean = {
|
||||
val matches = answer.toLowerCase().replaceAll(" ", "").equals(secret)
|
||||
if (!matches) {
|
||||
println(s"Didn't match, answer: '$answer' to secret '$secret'")
|
||||
}
|
||||
matches
|
||||
}
|
||||
|
||||
private def simpleText(text: String): Array[Byte] = {
|
||||
val img = new BufferedImage(350, 100, BufferedImage.TYPE_INT_RGB)
|
||||
val font = new Font("Arial", Font.ROMAN_BASELINE, 56)
|
||||
val graphics2D = img.createGraphics()
|
||||
val textLayout = new TextLayout(text, font, graphics2D.getFontRenderContext())
|
||||
HelperFunctions.setRenderingHints(graphics2D)
|
||||
graphics2D.setPaint(Color.WHITE)
|
||||
graphics2D.fillRect(0, 0, 350, 100)
|
||||
graphics2D.setPaint(Color.BLACK)
|
||||
textLayout.draw(graphics2D, 15, 50)
|
||||
graphics2D.dispose()
|
||||
val baos = new ByteArrayOutputStream()
|
||||
try {
|
||||
ImageIO.write(img, "png", baos)
|
||||
} catch {
|
||||
case e: Exception =>
|
||||
e.printStackTrace()
|
||||
}
|
||||
baos.toByteArray()
|
||||
}
|
||||
|
||||
def returnChallenge(): Challenge = {
|
||||
val secret = HelperFunctions.randomString(6, HelperFunctions.safeAlphabets)
|
||||
new Challenge(simpleText(secret), "image/png", secret.toLowerCase())
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import java.awt.Font
|
|||
import java.awt.Color
|
||||
import lc.captchas.interfaces.ChallengeProvider
|
||||
import lc.captchas.interfaces.Challenge
|
||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
||||
import java.util.{List => JavaList, Map => JavaMap}
|
||||
|
||||
class FilterChallenge extends ChallengeProvider {
|
||||
|
|
@ -18,13 +17,11 @@ class FilterChallenge extends ChallengeProvider {
|
|||
}
|
||||
|
||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||
val supportedParams = Map(
|
||||
"supportedLevels" -> JavaList.of("medium", "hard"),
|
||||
"supportedMedia" -> JavaList.of("image/png"),
|
||||
"supportedInputType" -> JavaList.of("text")
|
||||
).asJava
|
||||
|
||||
supportedParams
|
||||
JavaMap.of(
|
||||
"supportedLevels",JavaList.of("medium", "hard"),
|
||||
"supportedMedia", JavaList.of("image/png"),
|
||||
"supportedInputType", JavaList.of("text")
|
||||
)
|
||||
}
|
||||
|
||||
def returnChallenge(): Challenge = {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import java.awt.image.BufferedImage
|
|||
import java.awt.Color
|
||||
import lc.captchas.interfaces.ChallengeProvider
|
||||
import lc.captchas.interfaces.Challenge
|
||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
||||
import java.util.{List => JavaList, Map => JavaMap}
|
||||
|
||||
class LabelCaptcha extends ChallengeProvider {
|
||||
|
|
@ -30,13 +29,11 @@ class LabelCaptcha extends ChallengeProvider {
|
|||
}
|
||||
|
||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||
val supportedParams = Map(
|
||||
"supportedLevels" -> JavaList.of("hard"),
|
||||
"supportedMedia" -> JavaList.of("image/png"),
|
||||
"supportedInputType" -> JavaList.of("text")
|
||||
).asJava
|
||||
|
||||
supportedParams
|
||||
JavaMap.of(
|
||||
"supportedLevels", JavaList.of("hard"),
|
||||
"supportedMedia", JavaList.of("image/png"),
|
||||
"supportedInputType", JavaList.of("text")
|
||||
)
|
||||
}
|
||||
|
||||
def returnChallenge(): Challenge =
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import javax.imageio.stream.MemoryCacheImageOutputStream;
|
|||
import lc.captchas.interfaces.ChallengeProvider
|
||||
import lc.captchas.interfaces.Challenge
|
||||
import lc.misc.GifSequenceWriter
|
||||
import scala.jdk.CollectionConverters.MapHasAsJava
|
||||
import java.util.{List => JavaList, Map => JavaMap}
|
||||
|
||||
class Drop {
|
||||
|
|
@ -38,13 +37,11 @@ class RainDropsCP extends ChallengeProvider {
|
|||
}
|
||||
|
||||
def supportedParameters(): JavaMap[String, JavaList[String]] = {
|
||||
val supportedParams = Map(
|
||||
"supportedLevels" -> JavaList.of("medium", "easy"),
|
||||
"supportedMedia" -> JavaList.of("image/gif"),
|
||||
"supportedInputType" -> JavaList.of("text")
|
||||
).asJava
|
||||
|
||||
supportedParams
|
||||
JavaMap.of(
|
||||
"supportedLevels", JavaList.of("medium", "easy"),
|
||||
"supportedMedia", JavaList.of("image/gif"),
|
||||
"supportedInputType", JavaList.of("text")
|
||||
)
|
||||
}
|
||||
|
||||
private def extendDrops(drops: Array[Drop], steps: Int, xOffset: Int) = {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ object CaptchaProviders {
|
|||
//"FontFunCaptcha" -> new FontFunCaptcha,
|
||||
"GifCaptcha" -> new GifCaptcha,
|
||||
"ShadowTextCaptcha" -> new ShadowTextCaptcha,
|
||||
"RainDropsCaptcha" -> new RainDropsCP
|
||||
"RainDropsCaptcha" -> new RainDropsCP,
|
||||
"DebugCaptcha" -> new DebugCaptcha,
|
||||
//"LabelCaptcha" -> new LabelCaptcha
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -44,25 +44,9 @@ object Config {
|
|||
case JField("config", JObject(config)) => ("config", JString(config.toString))
|
||||
}
|
||||
val captchaConfig: List[CaptchaConfig] = captchaConfigTransform.extract[List[CaptchaConfig]]
|
||||
val allowedLevels: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDLEVELS.toString)
|
||||
val allowedMedia: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDMEDIA.toString)
|
||||
val allowedInputType: Set[String] = getAllValues(configJson, ParametersEnum.ALLOWEDINPUTTYPE.toString)
|
||||
|
||||
private def getAllValues(config: JValue, param: String): Set[String] = {
|
||||
val configValues = (config \\ param)
|
||||
val result = for {
|
||||
JObject(child) <- configValues
|
||||
JField(param) <- child
|
||||
} yield (param)
|
||||
|
||||
var valueSet = Set[String]()
|
||||
for (valueList <- result) {
|
||||
for (value <- valueList._2.children) {
|
||||
valueSet += value.values.toString
|
||||
}
|
||||
}
|
||||
valueSet
|
||||
}
|
||||
val allowedLevels: Set[String] = captchaConfig.flatMap(_.allowedLevels).toSet
|
||||
val allowedMedia: Set[String] = captchaConfig.flatMap(_.allowedMedia).toSet
|
||||
val allowedInputType: Set[String] = captchaConfig.flatMap(_.allowedInputType).toSet
|
||||
|
||||
private def getDefaultConfig(): String = {
|
||||
val defaultConfigMap =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"randomSeed" : 20,
|
||||
"port" : 8888,
|
||||
"captchaExpiryTimeLimit" : 5,
|
||||
"throttle" : 10,
|
||||
"threadDelay" : 2,
|
||||
"captchas" : [ {
|
||||
"name" : "DebugCaptcha",
|
||||
"allowedLevels" : [ "debug" ],
|
||||
"allowedMedia" : [ "image/png" ],
|
||||
"allowedInputType" : [ "text" ],
|
||||
"config" : { }
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
from locust import task, between, SequentialTaskSet
|
||||
from locust.contrib.fasthttp import FastHttpUser
|
||||
from locust import events
|
||||
import json
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
@events.quitting.add_listener
|
||||
def _(environment, **kw):
|
||||
totalStats = environment.stats.total
|
||||
if totalStats.fail_ratio > 0.20:
|
||||
logging.error("Test failed due to failure ratio " + totalStats.fail_ratio + " > 20%")
|
||||
environment.process_exit_code = 1
|
||||
elif totalStats.get_response_time_percentile(0.80) > 800:
|
||||
logging.error("Test failed due to 80th percentile response time > 800 ms")
|
||||
environment.process_exit_code = 1
|
||||
else:
|
||||
environment.process_exit_code = 0
|
||||
|
||||
class QuickStartUser(SequentialTaskSet):
|
||||
wait_time = between(0.1,0.2)
|
||||
|
||||
@task
|
||||
def captcha(self):
|
||||
captcha_params = {"level":"debug","media":"image/png","input_type":"text"}
|
||||
|
||||
with self.client.post(path="/v1/captcha", json=captcha_params, name="/captcha", catch_response = True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
captchaJson = resp.json()
|
||||
uuid = captchaJson.get("id")
|
||||
if not uuid:
|
||||
resp.failure("uuid not returned on /captcha endpoint: " + resp.text)
|
||||
|
||||
with self.client.get(path="/v1/media?id=%s" % uuid, name="/media", stream=True, catch_response = True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
|
||||
media = resp.content
|
||||
|
||||
ocrAnswer = self.solve(uuid, media)
|
||||
|
||||
answerBody = {"answer": ocrAnswer,"id": uuid}
|
||||
with self.client.post(path='/v1/answer', json=answerBody, name="/answer", catch_response=True) as resp:
|
||||
if resp.status_code != 200:
|
||||
resp.failure("Status was not 200: " + resp.text)
|
||||
else:
|
||||
if resp.json().get("result") != "True":
|
||||
resp.failure("Answer was not accepted: " + ocrAnswer)
|
||||
|
||||
def solve(self, uuid, media):
|
||||
mediaFileName = "tests/test-%s.png" % uuid
|
||||
with open(mediaFileName, "wb") as f:
|
||||
f.write(media)
|
||||
#ocrResult = subprocess.Popen("gocr %s" % mediaFileName, shell=True, stdout=subprocess.PIPE)
|
||||
ocrResult = subprocess.Popen("tesseract %s stdout -l eng" % mediaFileName, shell=True, stdout=subprocess.PIPE)
|
||||
ocrAnswer = ocrResult.stdout.readlines()[0].strip().decode()
|
||||
return ocrAnswer
|
||||
|
||||
|
||||
|
||||
class User(FastHttpUser):
|
||||
wait_time = between(0.1,0.2)
|
||||
tasks = [QuickStartUser]
|
||||
host = "http://localhost:8888"
|
||||
22
tests/run.sh
22
tests/run.sh
|
|
@ -7,9 +7,27 @@ java -jar target/scala-2.13/LibreCaptcha.jar &
|
|||
JAVA_PID=$!
|
||||
sleep 4
|
||||
|
||||
locust --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py
|
||||
locust --only-summary --headless -u 300 -r 100 --run-time 4m --stop-timeout 30 -f tests/locustfile.py
|
||||
status=$?
|
||||
|
||||
kill $JAVA_PID
|
||||
if [ $status != 0 ]; then
|
||||
exit $status
|
||||
fi
|
||||
|
||||
kill $JAVA_PID
|
||||
sleep 4
|
||||
|
||||
echo Run functional test
|
||||
cp data/config.json data/config.json.bak
|
||||
cp tests/debug-config.json data/config.json
|
||||
|
||||
java -jar target/scala-2.13/LibreCaptcha.jar &
|
||||
JAVA_PID=$!
|
||||
sleep 4
|
||||
|
||||
locust --only-summary --headless -u 1 -r 1 --run-time 1m --stop-timeout 30 -f tests/locustfile-functional.py
|
||||
status=$?
|
||||
mv data/config.json.bak data/config.json
|
||||
|
||||
kill $JAVA_PID
|
||||
exit $status
|
||||
|
|
|
|||
Loading…
Reference in New Issue