Merged dev into feature/workflows-support

This commit is contained in:
2025-07-14 19:44:38 -05:00
11 changed files with 292 additions and 110 deletions

View File

@ -0,0 +1,26 @@
#!/bin/bash
############################################################################
## Script to collect all JaCoCo reports from different modules into a
## single directory for easier artifact storage in CI.
############################################################################
mkdir -p /home/circleci/jacoco-reports/
##############################################################
## Find all module directories that have target/site/jacoco ##
##############################################################
for module_dir in */; do
if [ -d "${module_dir}target/site/jacoco" ]; then
module_name=$(basename "${module_dir%/}")
target_dir="/home/circleci/jacoco-reports/${module_name}"
echo "Collecting JaCoCo reports for module: ${module_name}"
cp -r "${module_dir}target/site/jacoco" "${target_dir}"
echo "Copied JaCoCo reports for ${module_name} to ${target_dir}"
fi
done
echo "All JaCoCo reports collected to /home/circleci/jacoco-reports/"

View File

@ -0,0 +1,48 @@
#!/bin/bash
############################################################################
## Script to concatenate all .txt files in the surefire-reports directory
## into a single artifact that can be stored in CI.
############################################################################
mkdir -p /home/circleci/test-output-artifacts/
###################################################################
## Find all module directories that have target/surefire-reports ##
###################################################################
for module_dir in */; do
if [ -d "${module_dir}target/surefire-reports" ]; then
module_name=$(basename "${module_dir%/}")
output_file="/home/circleci/test-output-artifacts/${module_name}-test-output.txt"
echo "Processing module: ${module_name}"
echo "Output file: ${output_file}"
##################################################################
## Concatenate all .txt files in the surefire-reports directory ##
##################################################################
if [ -n "$(find "${module_dir}target/surefire-reports" -name "*.txt" -type f)" ]; then
echo "=== Test Output for ${module_name} ===" > "${output_file}"
echo "Generated at: $(date)" >> "${output_file}"
echo "==========================================" >> "${output_file}"
echo "" >> "${output_file}"
##############################################
## Sort files to ensure consistent ordering ##
##############################################
find "${module_dir}target/surefire-reports" -name "*.txt" -type f | sort | while read -r txt_file; do
echo "--- File: $(basename "${txt_file}") ---" >> "${output_file}"
cat "${txt_file}" >> "${output_file}"
echo "" >> "${output_file}"
echo "--- End of $(basename "${txt_file}") ---" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
echo "" >> "${output_file}"
done
echo "Concatenated test output for ${module_name} to ${output_file}"
else
echo "No .txt files found in ${module_dir}target/surefire-reports"
fi
fi
done

View File

@ -5,35 +5,7 @@ orbs:
browser-tools: circleci/browser-tools@1.4.7 browser-tools: circleci/browser-tools@1.4.7
commands: commands:
store_jacoco_site: mvn_build:
parameters:
module:
type: string
steps:
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/index.html
when: always
- store_artifacts:
path: << parameters.module >>/target/site/jacoco/jacoco-resources
when: always
install_java17:
steps:
- run:
name: Install Java 17
command: |
sudo apt-get update
sudo apt install -y openjdk-17-jdk
sudo rm /etc/alternatives/java
sudo ln -s /usr/lib/jvm/java-17-openjdk-amd64/bin/java /etc/alternatives/java
- run:
## used by jacoco uncovered class reporting in pom.xml
name: Install html2text
command: |
sudo apt-get update
sudo apt-get install -y html2text
mvn_verify:
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@ -45,30 +17,41 @@ commands:
name: Write .env name: Write .env
command: | command: |
echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env echo "RDBMS_PASSWORD=$RDBMS_PASSWORD" >> qqq-sample-project/.env
- run:
name: Run Maven Compile
command: |
mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress compile
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
mvn_verify:
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "pom.xml" }}
- run: - run:
name: Run Maven Verify name: Run Maven Verify
command: | command: |
mvn -s .circleci/mvn-settings.xml -T4 verify mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress verify
- store_jacoco_site: - run:
module: qqq-backend-core name: Collect JaCoCo reports
- store_jacoco_site: command: .circleci/collect-jacoco-reports.sh
module: qqq-backend-module-filesystem when: always
- store_jacoco_site: - store_artifacts:
module: qqq-backend-module-rdbms path: /home/circleci/jacoco-reports
- store_jacoco_site: destination: jacoco-reports
module: qqq-backend-module-api when: always
- store_jacoco_site: - run:
module: qqq-middleware-api name: Concatenate test output files
- store_jacoco_site: command: .circleci/concatenate-test-output.sh
module: qqq-middleware-javalin when: always
- store_jacoco_site: - store_artifacts:
module: qqq-middleware-picocli path: /home/circleci/test-output-artifacts
- store_jacoco_site: destination: test-output
module: qqq-middleware-slack when: always
- store_jacoco_site:
module: qqq-language-support-javascript
- store_jacoco_site:
module: qqq-sample-project
- run: - run:
name: Save test results name: Save test results
command: | command: |
@ -77,10 +60,6 @@ commands:
when: always when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
- save_cache:
paths:
- ~/.m2
key: v1-dependencies-{{ checksum "pom.xml" }}
check_middleware_api_versions: check_middleware_api_versions:
steps: steps:
@ -91,8 +70,8 @@ commands:
- run: - run:
name: Build and Run ValidateApiVersions name: Build and Run ValidateApiVersions
command: | command: |
mvn -s .circleci/mvn-settings.xml -T4 install -DskipTests mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress install -DskipTests
mvn -s .circleci/mvn-settings.xml -pl qqq-middleware-javalin package appassembler:assemble -DskipTests mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress -pl qqq-middleware-javalin package appassembler:assemble -DskipTests
qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd) qqq-middleware-javalin/target/appassembler/bin/ValidateApiVersions -r $(pwd)
mvn_jar_deploy: mvn_jar_deploy:
@ -108,7 +87,7 @@ commands:
- run: - run:
name: Run Maven Jar Deploy name: Run Maven Jar Deploy
command: | command: |
mvn -s .circleci/mvn-settings.xml -T4 flatten:flatten jar:jar deploy:deploy mvn -s .circleci/mvn-settings.xml -T4 --no-transfer-progress flatten:flatten jar:jar deploy:deploy
- save_cache: - save_cache:
paths: paths:
- ~/.m2 - ~/.m2
@ -135,19 +114,25 @@ commands:
when: always when: always
jobs: jobs:
mvn_test: build:
executor: localstack/default
steps:
- mvn_build
test:
executor: localstack/default executor: localstack/default
steps: steps:
## - localstack/startup
- install_java17
- mvn_verify - mvn_verify
api_version_check:
executor: localstack/default
steps:
- check_middleware_api_versions - check_middleware_api_versions
mvn_deploy: mvn_deploy:
executor: localstack/default executor: localstack/default
steps: steps:
## - localstack/startup - mvn_build
- install_java17
- mvn_verify - mvn_verify
- check_middleware_api_versions - check_middleware_api_versions
- mvn_jar_deploy - mvn_jar_deploy
@ -161,13 +146,31 @@ jobs:
workflows: workflows:
test_only: test_only:
jobs: jobs:
- mvn_test: - build:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ] context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
filters: filters:
branches: branches:
ignore: /(dev|integration.*)/ ignore: /(dev|integration.*)/
tags: tags:
ignore: /(version|snapshot)-.*/ ignore: /(version|snapshot)-.*/
- test:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
- api_version_check:
context: [ qqq-maven-registry-credentials, build-qqq-sample-app ]
requires:
- build
filters:
branches:
ignore: /(dev|integration.*)/
tags:
ignore: /(version|snapshot)-.*/
deploy: deploy:
jobs: jobs:

View File

@ -30,6 +30,20 @@ There are a few useful IntelliJ settings files, under `qqq-dev-tools/intellij`:
One will likely also want the [Kingsrook Commentator One will likely also want the [Kingsrook Commentator
Plugin](https://plugins.jetbrains.com/plugin/19325-kingsrook-commentator). Plugin](https://plugins.jetbrains.com/plugin/19325-kingsrook-commentator).
## Test Logging
By default, when ran from the command line, mvn surefire will make each test's
output (e.g., System.out, err, printStackTrace, and all logger calls) go into a
file under target/surefire-reports/${className}.txt.
The system property `-DtestOutputToFile=false` can be given on the command line
to get all of this output on the console.
In the IDE (e.g,. IntelliJ), output goes to the Console.
In CircleCI, output goes to files, and those files are concatenated together and
stored as artifacts.
## License ## License
QQQ - Low-code Application Framework for Engineers. \ QQQ - Low-code Application Framework for Engineers. \
Copyright (C) 2020-2024. Kingsrook, LLC \ Copyright (C) 2020-2024. Kingsrook, LLC \

32
pom.xml
View File

@ -59,6 +59,7 @@
<coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum> <coverage.instructionCoveredRatioMinimum>0.80</coverage.instructionCoveredRatioMinimum>
<coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum> <coverage.classCoveredRatioMinimum>0.95</coverage.classCoveredRatioMinimum>
<plugin.shade.phase>none</plugin.shade.phase> <plugin.shade.phase>none</plugin.shade.phase>
<testOutputToFile>true</testOutputToFile>
</properties> </properties>
<profiles> <profiles>
@ -141,6 +142,8 @@
<configuration> <configuration>
<!-- Sets the VM argument line used when integration tests are run. --> <!-- Sets the VM argument line used when integration tests are run. -->
<argLine>@{jaCoCoArgLine}</argLine> <argLine>@{jaCoCoArgLine}</argLine>
<!-- Reduce console output for cleaner JUnit output -->
<redirectTestOutputToFile>${testOutputToFile}</redirectTestOutputToFile>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
@ -244,30 +247,29 @@ if [ ! -e target/site/jacoco/index.html ]; then
fi fi
echo echo
echo "Jacoco coverage summary report:" echo "Jacoco coverage summary report for module: ${project.artifactId}"
echo " See also target/site/jacoco/index.html" echo " See also target/site/jacoco/index.html"
echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html" echo " and https://www.jacoco.org/jacoco/trunk/doc/counters.html"
echo "------------------------------------------------------------" echo "------------------------------------------------------------"
if which xpath > /dev/null 2>&1; then # Parse Jacoco HTML coverage summary
echo "Element\nInstructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers if [ -f target/site/jacoco/index.html ]; then
xpath -n -q -e '/html/body/table/tfoot/tr[1]/td/text()' target/site/jacoco/index.html > /tmp/$$.values echo -e "Instructions Missed\nInstruction Coverage\nBranches Missed\nBranch Coverage\nComplexity Missed\nComplexity Hit\nLines Missed\nLines Hit\nMethods Missed\nMethods Hit\nClasses Missed\nClasses Hit\n" > /tmp/$$.headers
sed 's/<\/\w\+>/&\n/g' target/site/jacoco/index.html | grep -A 12 '<tfoot>' | grep '<td' | sed 's/<td class="\w\+\d*">\([^<]*\)<\/td>/\1/' | grep -v Total > /tmp/$$.values
paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}' paste /tmp/$$.headers /tmp/$$.values | tail +2 | awk -v FS='\t' '{printf("%-20s %s\n",$1,$2)}'
rm /tmp/$$.headers /tmp/$$.values rm /tmp/$$.headers /tmp/$$.values
else else
echo "xpath is not installed. Jacoco coverage summary will not be produced here..."; echo "Jacoco coverage summary was not found.";
fi fi
echo "-----------------------------"
echo
if which html2text > /dev/null 2>&1; then echo "Untested classes, per Jacoco for module: ${project.artifactId}"
echo "Untested classes, per Jacoco:" echo "-----------------------------"
echo "-----------------------------" # Parse Jacoco XML reports directly to find classes with 0% coverage
for i in target/site/jacoco/*/index.html; do sed 's/<classs .*\?>/&\n/g;s/<\/class>/&\n/g' target/site/jacoco/jacoco.xml | grep -v 'counter type="CLASS" missed="0"' | sed 's/>.*//;s/.*\///;s/".*//'
html2text -width 500 -nobs $i | sed '1,/^Total/d;' | grep -v Created | sed 's/ \+/ /g' | sed 's/ [[:digit:]]$//' | grep -v 0$ | cut -d' ' -f1; echo "-----------------------------"
done; echo
echo
else
echo "html2text is not installed. Untested classes from Jacoco will not be printed here...";
fi
]]> ]]>
</argument> </argument>

View File

@ -55,11 +55,6 @@
<artifactId>sshd-sftp</artifactId> <artifactId>sshd-sftp</artifactId>
<version>2.14.0</version> <version>2.14.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.sshd</groupId>
<artifactId>sshd-sftp</artifactId>
<version>2.14.0</version>
</dependency>
<dependency> <dependency>
<groupId>cloud.localstack</groupId> <groupId>cloud.localstack</groupId>

View File

@ -122,6 +122,18 @@
<build> <build>
<sourceDirectory>src/main/java</sourceDirectory> <sourceDirectory>src/main/java</sourceDirectory>
<plugins> <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
<configuration>
<additionalClasspathElements>
<additionalClasspathElement>
${project.basedir}/src/test/resources/static-site.jar
</additionalClasspathElement>
</additionalClasspathElements>
</configuration>
</plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>

View File

@ -1932,6 +1932,7 @@ public class QJavalinImplementation
{ {
String searchTerm = context.queryParam("searchTerm"); String searchTerm = context.queryParam("searchTerm");
String ids = context.queryParam("ids"); String ids = context.queryParam("ids");
String labels = context.queryParam("labels");
SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput(); SearchPossibleValueSourceInput input = new SearchPossibleValueSourceInput();
setupSession(context, input); setupSession(context, input);
@ -1945,6 +1946,11 @@ public class QJavalinImplementation
List<Serializable> idList = new ArrayList<>(Arrays.asList(ids.split(","))); List<Serializable> idList = new ArrayList<>(Arrays.asList(ids.split(",")));
input.setIdList(idList); input.setIdList(idList);
} }
else if(StringUtils.hasContent(labels))
{
List<String> labelList = new ArrayList<>(Arrays.asList(labels.split(",")));
input.setLabelList(labelList);
}
SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input); SearchPossibleValueSourceOutput output = new SearchPossibleValueSourceAction().execute(input);

View File

@ -49,16 +49,14 @@ import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInterface
{ {
private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class); private static final QLogger LOG = QLogger.getLogger(SimpleFileSystemDirectoryRouter.class);
public static boolean loadStaticFilesFromJar = false;
private final String hostedPath; private final String hostedPath;
private final String fileSystemPath; private final String fileSystemPath;
private QCodeReference routeAuthenticator; private QCodeReference routeAuthenticator;
private QInstance qInstance; private QInstance qInstance;
/******************************************************************************* /*******************************************************************************
** Constructor ** Constructor
** **
@ -67,6 +65,24 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
{ {
this.hostedPath = hostedPath; this.hostedPath = hostedPath;
this.fileSystemPath = fileSystemPath; this.fileSystemPath = fileSystemPath;
///////////////////////////////////////////////////////////////////////////////////////////////////////
// read the property to see if we should load static files from the jar file or from the file system //
// Javan only supports loading via one method per path, so its a choice of one or the other... //
///////////////////////////////////////////////////////////////////////////////////////////////////////
try
{
String propertyName = "qqq.javalin.enableStaticFilesFromJar"; // TODO: make a more general way to handle properties like this system-wide via a central config class
String propertyValue = System.getProperty(propertyName, "");
if(propertyValue.equals("true"))
{
loadStaticFilesFromJar = true;
}
}
catch(Exception e)
{
e.printStackTrace();
}
} }
@ -98,25 +114,41 @@ public class SimpleFileSystemDirectoryRouter implements QJavalinRouteProviderInt
***************************************************************************/ ***************************************************************************/
private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig) private void handleJavalinStaticFileConfig(StaticFileConfig staticFileConfig)
{ {
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
if(resource == null)
{
String message = "Could not find file system path: " + fileSystemPath;
if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
{
message += ". For non-absolute paths, do not prefix with a leading slash.";
}
throw new RuntimeException(message);
}
if(!hostedPath.startsWith("/")) if(!hostedPath.startsWith("/"))
{ {
LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash..."); LOG.warn("hostedPath [" + hostedPath + "] should probably start with a leading slash...");
} }
staticFileConfig.directory = resource.getFile(); /// /////////////////////////////////////////////////////////////////////////////////////
staticFileConfig.hostedPath = hostedPath; // Handle loading static files from the jar OR the filesystem based on system property //
staticFileConfig.location = Location.EXTERNAL; /// /////////////////////////////////////////////////////////////////////////////////////
if(SimpleFileSystemDirectoryRouter.loadStaticFilesFromJar)
{
staticFileConfig.directory = fileSystemPath;
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.CLASSPATH;
LOG.info("Static File Config : hostedPath [" + hostedPath + "] : directory [" + staticFileConfig.directory + "] : location [CLASSPATH]");
}
else
{
URL resource = getClass().getClassLoader().getResource(fileSystemPath);
if(resource == null)
{
String message = "Could not find file system path: " + fileSystemPath;
if(fileSystemPath.startsWith("/") && getClass().getClassLoader().getResource(fileSystemPath.replaceFirst("^/+", "")) != null)
{
message += ". For non-absolute paths, do not prefix with a leading slash.";
}
throw new RuntimeException(message);
}
staticFileConfig.directory = resource.getFile();
staticFileConfig.hostedPath = hostedPath;
staticFileConfig.location = Location.EXTERNAL;
LOG.info("Static File Config : hostedPath [" + hostedPath + "] : directory [" + staticFileConfig.directory + "] : location [EXTERNAL]");
}
} }

View File

@ -29,6 +29,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication; import com.kingsrook.qqq.backend.core.instances.AbstractQQQApplication;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.javalin.TestUtils; import com.kingsrook.qqq.backend.javalin.TestUtils;
import com.kingsrook.qqq.middleware.javalin.routeproviders.SimpleFileSystemDirectoryRouter;
import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1; import com.kingsrook.qqq.middleware.javalin.specs.v1.MiddlewareVersionV1;
import io.javalin.http.HttpStatus; import io.javalin.http.HttpStatus;
import kong.unirest.HttpResponse; import kong.unirest.HttpResponse;
@ -52,6 +53,16 @@ class QApplicationJavalinServerTest
/***************************************************************************
**
***************************************************************************/
private static AbstractQQQApplication getQqqApplication()
{
return new TestApplication();
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -60,6 +71,7 @@ class QApplicationJavalinServerTest
{ {
javalinServer.stop(); javalinServer.stop();
TestApplication.callCount = 0; TestApplication.callCount = 0;
System.clearProperty("qqq.javalin.enableStaticFilesFromJar");
} }
@ -196,6 +208,48 @@ class QApplicationJavalinServerTest
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticRouterFilesFromExternal() throws Exception
{
System.setProperty("qqq.javalin.enableStaticFilesFromJar", "false");
javalinServer = new QApplicationJavalinServer(getQqqApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT);
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served/foo.html").asString();
assertEquals("Foo? Bar!", response.getBody());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testStaticRouterFilesFromClasspath() throws Exception
{
System.setProperty("qqq.javalin.enableStaticFilesFromJar", "true");
javalinServer = new QApplicationJavalinServer(new QApplicationJavalinServerTest.TestApplication())
.withServeFrontendMaterialDashboard(false)
.withPort(PORT)
.withAdditionalRouteProvider(new SimpleFileSystemDirectoryRouter("/statically-served-from-jar", "static-site-from-jar/"));
javalinServer.start();
Unirest.config().setDefaultResponseEncoding("UTF-8");
HttpResponse<String> response = Unirest.get("http://localhost:" + PORT + "/statically-served-from-jar/foo-in-jar.html").asString();
assertEquals("Foo in a Jar!\n", response.getBody());
}
/******************************************************************************* /*******************************************************************************
** **
*******************************************************************************/ *******************************************************************************/
@ -296,16 +350,6 @@ class QApplicationJavalinServerTest
/***************************************************************************
**
***************************************************************************/
private static AbstractQQQApplication getQqqApplication()
{
return new TestApplication();
}
/*************************************************************************** /***************************************************************************
** **
***************************************************************************/ ***************************************************************************/