diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..40e6f45 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,36 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: build-jdk-${{ matrix.version }} + runs-on: ubuntu-latest + strategy: + matrix: + version: [17, 21] + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: ${{ matrix.version }} + + - name: Cache m2 repo + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-${{ matrix.version }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.version }}-maven- + + - name: Build + run: mvn --batch-mode install + + - name: Run spotbugs check + run: mvn spotbugs:check diff --git a/.gitignore b/.gitignore index 524f096..0c45e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,13 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* + +# Maven build targets +target/ + +# IntelliJ IDEA specific +.idea/ +*.iml + +### Mac OS ### +**.DS_Store diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9fa5c83 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +dkornel@skodjob.io or jstejska@skodjob.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..423c46a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,52 @@ +# Contributing to Test Frame + +First off, thanks for taking the time to contribute! 🎉👍 The following is a set of guidelines for contributing to the Test Frame repository. + +## Code of Conduct + +By participating in this project, you agree to abide by the [Code of Conduct](CODE_OF_CONDUCT.md). + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please report it by opening an issue. When opening an issue, include: +- A clear and descriptive title. +- A detailed description of the problem. +- Steps to reproduce the issue. +- Any relevant logs or screenshots. + +### Suggesting Enhancements + +We welcome suggestions to improve the project. When suggesting enhancements, please: +- Use a clear and descriptive title. +- Provide a detailed explanation of the enhancement. +- Explain why this enhancement would be useful. + +### Pull Requests + +We welcome pull requests. If you are planning a major change, please open an issue first to discuss your plans. This helps avoid duplicate efforts and ensures that your contributions align with the project's goals. + +When you are ready to submit your pull request, please ensure that you: +- Follow the existing code style and conventions. +- Test your changes thoroughly. +- Provide a detailed description of your changes in the pull request. + +### Testing + +Every feature enhancement should be thoroughly tested. This includes writing both unit tests and integration tests. + +1. **Unit Tests** + + Ensure that you write unit tests for any new functionality you add. Place these tests in the appropriate test files within the main project directory. Unit tests should cover individual units of code to ensure they work as expected. + +### Style Guide + +Please follow the existing code style and conventions used in the project. This helps to maintain a consistent codebase. + +## Additional Resources + +- [GitHub Help](https://help.github.com/) +- [Understanding the GitHub Flow](https://guides.github.com/introduction/flow/) + +Thank you for contributing to Test Frame! diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..71956eb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities only for the latest released version of the library. To ensure you are receiving the latest security updates, please update to the latest version of the library. + +| Version | Supported | +| -------------- | ------------------ | +| Latest release | :white_check_mark: | +| Older versions | :x: | + +## Reporting a Vulnerability + +If you discover a security vulnerability, please email to security[at]skodjob.io. All security vulnerabilities will be promptly addressed. + +### Reporting Guidelines + +To help us address the issue as quickly as possible, please include the following details in your report: +- A description of the vulnerability and its potential impact. +- Detailed steps to reproduce the vulnerability. +- Any potential mitigations or workarounds. + +We kindly ask you to refrain from publicly disclosing the vulnerability until we have resolved it. + +### Response Process + +Upon receiving your report, we will: +1. Acknowledge the receipt of your report within 72 hours. +2. Investigate and validate the reported vulnerability. +3. Provide you with an estimated timeline for the fix. +4. Notify you when the vulnerability is fixed. + +We are committed to keeping our users safe and will do our utmost to address all security vulnerabilities in a timely manner. + +## Security Resources + +- [OWASP Top Ten](https://owasp.org/www-project-top-ten/) +- [CVE Details](https://www.cvedetails.com/) +- [National Vulnerability Database](https://nvd.nist.gov/) + +Thank you for helping us keep Test Frame secure! diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 0000000..3d8034d --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..096c7d7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,305 @@ + + + + 4.0.0 + + io.skodjob + load-generator + 0.0.1-SNAPSHOT + + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + load-generator + Reasonable load generator for Messaging + https://github.com/skodjob/load-generator + + + scm:git:git:/github.com/skodjob/load-generator.git + scm:git:ssh://github.com/skodjob/load-generator.git + https://github.com/skodjob/load-generator + + + + GitHub + https://github.com/skodjob/load-generator/issues + + + + + github + GitHub Apache Maven Packages + https://maven.pkg.github.com/skodjob/load-generator + + + + + + im-konge + Lukáš Král + lukywill16@gmail.com + + + kornys + David Kornel + kornys@outlook.com + + + Frawless + Jakub Stejskal + xstejs24@gmail.com + + + obabec + Ondrej Babec + ond.babec@gmail.com + + + see-quick + Maros Orsak + maros.orsak159@gmail.com + + + + + UTF-8 + 17 + 17 + 17 + + 3.3.0 + 3.1.1 + 3.1.0 + 0.4.0 + 3.4.1 + 3.4.2 + 3.9.0 + + 2.17.2 + 5.10.2 + 1.10.2 + 3.3.0 + + 2.17.1 + + + 4.8.6 + 4.8.6.1 + 3.3.0 + + true + + ${skipTests} + + checkstyle.xml + + + + + com.fasterxml.jackson.core + jackson-core + ${jackson-dataformat-yaml.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson-dataformat-yaml.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + + + org.junit.platform + junit-platform-commons + ${junit.platform.version} + + + org.junit.platform + junit-platform-launcher + ${junit.platform.version} + + + org.junit.platform + junit-platform-engine + ${junit.platform.version} + + + + + org.apache.logging.log4j + log4j-api + ${log4j.version} + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + com.github.spotbugs + spotbugs-annotations + ${spotbugs.version} + provided + + + + + + + src/main/resources + + **/*.* + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${maven.spotbugs.version} + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + Max + + Low + + true + + ${project.build.directory}/spotbugs + + + + org.apache.maven.plugins + maven-javadoc-plugin + ${maven.javadoc.version} + + + attach-javadocs + + jar + + + public + true + true + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven.checkstyle.version} + + + validate + validate + + true + true + + + check + + + + + + maven-source-plugin + ${maven.source.plugin.version} + + + attach-sources + deploy + + jar-no-fork + + + + + + maven-deploy-plugin + ${maven.deploy.plugin.version} + + + deploy + deploy + + deploy + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + ${maven.surefire.version} + + + + verify + integration-test + + + + + ${it.skip} + 1 + + **/IT*.java + **/*IT.java + + + + junit.jupiter.extensions.autodetection.enabled = true + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.surefire.version} + true + + + + junit.jupiter.extensions.autodetection.enabled = true + + + ${ut.skip} + + + + + diff --git a/src/main/java/io/skodjob/loadgenerator/DataGenerator.java b/src/main/java/io/skodjob/loadgenerator/DataGenerator.java new file mode 100644 index 0000000..6b993e8 --- /dev/null +++ b/src/main/java/io/skodjob/loadgenerator/DataGenerator.java @@ -0,0 +1,92 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.skodjob.loadgenerator.enums.ETemplateType; +import io.skodjob.loadgenerator.handlers.IotDevice; +import io.skodjob.loadgenerator.handlers.People; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +/** + * This class is responsible for generating data based on specified templates. + */ +public class DataGenerator { + private static final Logger LOGGER = LogManager.getLogger(DataGenerator.class); + + private final String templateJson; + private final ETemplateType templateType; + + /** + * Constructor for DataGenerator. + * + * @param templateType the type of template to be used for data generation + */ + // The suppression is needed due to false positive result + @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") + public DataGenerator(ETemplateType templateType) { + this.templateType = Objects.requireNonNull(templateType, "TemplateType cannot be null!"); + try { + this.templateJson = loadTemplate(templateType.getTemplatePath()); + } catch (IOException e) { + LOGGER.error("Error loading template", e); + throw new RuntimeException("Failed to load template: " + templateType.getTemplatePath(), e); + } + + LOGGER.info("Loaded {} template with location {}", templateType.getTemplateName(), + templateType.getTemplatePath()); + } + + /** + * Loads the template from the specified path. + * + * @param templateName the name of the template file + * @return the content of the template file as a string + * @throws IOException if an I/O error occurs while loading the template + */ + private String loadTemplate(String templateName) throws IOException { + try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream(templateName)) { + if (inputStream == null) { + throw new IOException("Template not found: " + templateName); + } + return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + /** + * Generates string data based on the template type. + * + * @return the generated string data + */ + public String generateStringData() { + switch (this.templateType) { + case PAYROLL_EMPLOYEE: + return Utils.stripWhitespace(People.fillTemplate(this.templateJson)); + case IOT_DEVICE: + return Utils.stripWhitespace(IotDevice.fillTemplate(this.templateJson)); + default: + throw new IllegalArgumentException("Unknown template type: " + this.templateType); + } + } + + /** + * Generates JSON data based on the template type. + * + * @return the generated JSON data + * @throws IOException if an I/O error occurs while generating JSON data + */ + public JsonNode generateJsonData() throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + return objectMapper.readTree(generateStringData()); + } +} diff --git a/src/main/java/io/skodjob/loadgenerator/Utils.java b/src/main/java/io/skodjob/loadgenerator/Utils.java new file mode 100644 index 0000000..e18d7aa --- /dev/null +++ b/src/main/java/io/skodjob/loadgenerator/Utils.java @@ -0,0 +1,64 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility class providing common methods for data loading and JSON manipulation. + */ +public class Utils { + + private Utils() {} + + /** + * Loads data from a file and returns it as a list of strings. + * + * @param filename the name of the file to load + * @return a list of strings containing the data from the file + * @throws IOException if an I/O error occurs + */ + public static List loadData(String filename) throws IOException { + try (InputStream inputStream = Utils.class.getResourceAsStream(filename); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + return reader.lines().collect(Collectors.toList()); + } + } + + /** + * Removes all whitespace characters from a JSON string. + * + * @param jsonString the original JSON string + * @return the JSON string without whitespace characters + */ + public static String stripWhitespace(String jsonString) { + return jsonString.replaceAll("\\s+", ""); + } + + /** + * Converts a JSON string into a pretty-printed JSON format. + * + * @param jsonString the original JSON string + * @return the pretty-printed JSON string + * @throws IOException if an I/O error occurs + */ + public static String prettyPrintJson(String jsonString) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + + JsonNode jsonNode = objectMapper.readTree(jsonString); + return objectMapper.writeValueAsString(jsonNode); + } +} diff --git a/src/main/java/io/skodjob/loadgenerator/enums/ETemplateType.java b/src/main/java/io/skodjob/loadgenerator/enums/ETemplateType.java new file mode 100644 index 0000000..86df708 --- /dev/null +++ b/src/main/java/io/skodjob/loadgenerator/enums/ETemplateType.java @@ -0,0 +1,52 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator.enums; + +/** + * Enum representing different template types for use in the load generator. + */ +public enum ETemplateType { + /** + * Template for People Payrol data + */ + PAYROLL_EMPLOYEE("payroll_employee", "templates/payroll_employee.json"), + + /** + * Template for IoT device data + */ + IOT_DEVICE("iot_device", "templates/iot_device.json"); + + private final String templateName; + private final String templatePath; + + /** + * Constructor for ETemplateType. + * + * @param templateName the name of the template + * @param templatePath the path to the template file + */ + ETemplateType(String templateName, String templatePath) { + this.templateName = templateName; + this.templatePath = templatePath; + } + + /** + * Gets the name of the template. + * + * @return the name of the template + */ + public String getTemplateName() { + return templateName; + } + + /** + * Gets the path to the template file. + * + * @return the path to the template file + */ + public String getTemplatePath() { + return templatePath; + } +} diff --git a/src/main/java/io/skodjob/loadgenerator/handlers/IotDevice.java b/src/main/java/io/skodjob/loadgenerator/handlers/IotDevice.java new file mode 100644 index 0000000..92363b6 --- /dev/null +++ b/src/main/java/io/skodjob/loadgenerator/handlers/IotDevice.java @@ -0,0 +1,209 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator.handlers; + +import io.skodjob.loadgenerator.Utils; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Random; + +/** + * This class generates random IoT device data for use in templates. + */ +public class IotDevice { + private static final Random RANDOM = new Random(); + + private static final String STATE_ACTIVE = "active"; + private static final String STATE_INACTIVE = "inactive"; + private static final String STATE_ERROR = "error"; + private static final String STATE_ON = "on"; + private static final String STATE_OFF = "off"; + private static final List ACTIVITY_STATES = Arrays.asList(STATE_ACTIVE, STATE_INACTIVE, STATE_ERROR); + private static final List POWER_STATES = Arrays.asList(STATE_ON, STATE_OFF); + + private static final String BEHAVIOR_ON = STATE_ON; + private static final String BEHAVIOR_OFF = STATE_OFF; + private static final String BEHAVIOR_TOGGLE = "toggle"; + private static final String BEHAVIOR_PREVIOUS = "previous"; + private static final List BEHAVIORS = Arrays.asList( + BEHAVIOR_ON, BEHAVIOR_OFF, BEHAVIOR_TOGGLE, BEHAVIOR_PREVIOUS); + private static final List VENDORS; + + private static final String TYPE_LIGHT = "light"; + private static final String TYPE_BUTTON = "button"; + private static final String TYPE_THERMOMETER = "thermometer"; + private static final String TYPE_PLUG = "plug"; + private static final String TYPE_CUSTOM = "custom"; + private static final String TYPE_GATE = "gate"; + + private static final List TYPES = Arrays.asList(TYPE_LIGHT, TYPE_BUTTON, TYPE_THERMOMETER, + TYPE_PLUG, TYPE_CUSTOM, TYPE_GATE); + + static { + try { + VENDORS = Utils.loadData("/data/iot_vendors.txt"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Default constructor + */ + private IotDevice() { + + } + + /** + * Generates a random IoT device type. + * + * @return A random IoT device type. + */ + private static String generateType() { + return TYPES.get(RANDOM.nextInt(TYPES.size())); + } + + /** + * Generates a random IPv4 address. + * + * @return A random IPv4 address. + */ + private static String generateIPv4() { + return RANDOM.nextInt(256) + "." + RANDOM.nextInt(256) + "." + RANDOM.nextInt(256) + "." + RANDOM.nextInt(256); + } + + /** + * Generates a random activity state. + * + * @return A random activity state. + */ + private static String generateActivityState() { + return ACTIVITY_STATES.get(RANDOM.nextInt(ACTIVITY_STATES.size())); + } + + /** + * Generates a random power state. + * + * @return A random power state. + */ + private static String generatePowerState() { + return POWER_STATES.get(RANDOM.nextInt(POWER_STATES.size())); + } + + /** + * Generates a random MAC address. + * + * @return A random MAC address. + */ + private static String generateMacAddress() { + byte[] macAddr = new byte[6]; + RANDOM.nextBytes(macAddr); + + macAddr[0] = (byte) (macAddr[0] & (byte) 254); + + StringBuilder mac = new StringBuilder(18); + for (byte b : macAddr) { + if (mac.length() > 0) mac.append(":"); + mac.append(String.format("%02x", b)); + } + return mac.toString(); + } + + /** + * Generates a random ID. + * + * @return A random ID. + */ + private static String generateId() { + return String.valueOf(RANDOM.nextInt(1000000)); + } + + /** + * Generates the current timestamp. + * + * @return The current timestamp. + */ + private static String generateLastUpdate() { + return String.valueOf(System.currentTimeMillis()); + } + + /** + * Generates a random link quality (0-100). + * + * @return A random link quality. + */ + private static String generateLinkQuality() { + return String.format("%s", RANDOM.nextInt(101)); + } + + /** + * Generates data for a specific IoT device type. + * + * @param type The type of the IoT device. + * @return The generated data for the IoT device. + */ + private static String generateData(String type) { + switch (type) { + case TYPE_LIGHT: + return String.format("{\"power\": \"%s\", \"brightness\": %d, \"power_on_behavior\": \"%s\"}", + generatePowerState(), RANDOM.nextInt(255), generatePowerOnBehavior()); + case TYPE_PLUG: + return String.format("{\"power\": \"%s\", \"energy_current\": {\"state\": %.3f, \"unit\": \"A\"}, " + + "\"energy_today\": {\"state\": %.3f, \"unit\": \"kWh\"}}", + generatePowerState(), RANDOM.nextDouble(), RANDOM.nextDouble() * 10); + case TYPE_BUTTON: + return String.format("{\"power\": \"%s\", \"battery\": {\"value\": %d, \"unit\": \"%%\"}}", + generatePowerState(), RANDOM.nextInt(101)); + case TYPE_THERMOMETER: + return String.format("{\"temperature\": %.2f, \"humidity\": %.2f, " + + "\"battery\": {\"value\": %d, \"unit\": \"%%\"}}", + RANDOM.nextDouble() * 70 - 30, RANDOM.nextDouble() * 80 + 10, RANDOM.nextInt(101)); + case TYPE_GATE: + return String.format("{\"vendor\": \"%s\", \"state\": \"%s\"}", + generateVendor(), generateActivityState()); + default: + return String.format("{\"info\": \"custom data\", \"state\": \"%s\"}", generateActivityState()); + } + } + + /** + * Generates a random power-on behavior. + * + * @return A random power-on behavior. + */ + private static String generatePowerOnBehavior() { + return BEHAVIORS.get(RANDOM.nextInt(BEHAVIORS.size())); + } + + /** + * Generates a random vendor. + * + * @return A random vendor. + */ + private static String generateVendor() { + return VENDORS.get(RANDOM.nextInt(VENDORS.size())); + } + + /** + * Fills the template with random IoT device data. + * + * @param template The template to be filled. + * @return The filled template. + */ + public static String fillTemplate(String template) { + String type = generateType(); + template = template.replace("@ipv4", "\"%s\"".formatted(generateIPv4())); + template = template.replace("@mac", "\"%s\"".formatted(generateMacAddress())); + template = template.replace("@id", "\"%s\"".formatted(generateId())); + template = template.replace("@type", "\"" + type + "\""); + template = template.replace("@last_update", "\"%s\"".formatted(generateLastUpdate())); + template = template.replace("@link_quality", generateLinkQuality()); + template = template.replace("@data", generateData(type)); + + return template; + } +} diff --git a/src/main/java/io/skodjob/loadgenerator/handlers/People.java b/src/main/java/io/skodjob/loadgenerator/handlers/People.java new file mode 100644 index 0000000..c1dd0cf --- /dev/null +++ b/src/main/java/io/skodjob/loadgenerator/handlers/People.java @@ -0,0 +1,184 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator.handlers; + +import io.skodjob.loadgenerator.Utils; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * This class generates random people-related data for use in templates. + */ +public class People { + private static final Random RANDOM = new Random(); + + private static final List NAMES_F; + private static final List NAMES_M; + private static final List SURNAMES; + private static final List COMPANIES; + private static final List EMAIL_PROVIDERS; + + private static final String MALE = "Male"; + private static final String FEMALE = "Female"; + + static { + try { + NAMES_F = Utils.loadData("/data/nameF.txt"); + NAMES_M = Utils.loadData("/data/nameM.txt"); + SURNAMES = Utils.loadData("/data/surnames.txt"); + COMPANIES = Utils.loadData("/data/companies.txt"); + EMAIL_PROVIDERS = Utils.loadData("/data/mail_provider.txt"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Default constructor + */ + private People() { + + } + + /** + * Generates a random male first name. + * + * @return A random male first name. + */ + public static String generateMaleFirstName() { + return NAMES_M.get(RANDOM.nextInt(NAMES_M.size())); + } + + /** + * Generates a random female first name. + * + * @return A random female first name. + */ + public static String generateFemaleFirstName() { + return NAMES_F.get(RANDOM.nextInt(NAMES_F.size())); + } + + /** + * Generates a random surname. + * + * @return A random surname. + */ + public static String generateSurname() { + return SURNAMES.get(RANDOM.nextInt(SURNAMES.size())); + } + + /** + * Generates a random company name. + * + * @return A random company name. + */ + public static String generateCompany() { + return COMPANIES.get(RANDOM.nextInt(COMPANIES.size())); + } + + /** + * Generates a random email address. + * + * @param name The first name to be included in the email address. + * @param surname The surname to be included in the email address. + * @param company The company to be included in the email address. + * @return A random email address. + */ + public static String generateEmail(String name, String surname, String company) { + return "%s.%s@%s".formatted(name, surname, company); + } + + /** + * Generates a random age between 18 and 60. + * + * @return A random age between 18 and 60. + */ + public static String generateAge() { + return String.valueOf(18 + RANDOM.nextInt(43)); // Generate age between 18 and 60 + } + + /** + * Generates a random Social Security Number (SSN). + * + * @return A random SSN. + */ + public static String generateSSN() { + return String.format("%03d-%02d-%04d", + RANDOM.nextInt(1000), + RANDOM.nextInt(100), + RANDOM.nextInt(10000)); + } + + /** + * Generates a random hourly rate between $10.00 and $50.00. + * + * @return A random hourly rate. + */ + public static String generateHourlyRate() { + // Generate hourly rate between $10.00 and $50.00 + return String.format("%.2f", 10 + (RANDOM.nextDouble() * 40)); + } + + /** + * Generates a random gender. + * + * @return A random gender. + */ + public static String generateGender() { + return RANDOM.nextBoolean() ? MALE : FEMALE; + } + + /** + * Generates a random employee ID. + * + * @return A random employee ID. + */ + public static String generateEmployeeId() { + return String.format("%s", (1000 + RANDOM.nextInt(900000))); + } + + /** + * Generates a random email provider. + * + * @return A random email provider. + */ + public static String generateEmailProvider() { + return EMAIL_PROVIDERS.get(RANDOM.nextInt(EMAIL_PROVIDERS.size())); + } + + /** + * Fills the template with random people-related data. + * + * @param template The template to be filled. + * @return The filled template. + */ + public static String fillTemplate(String template) { + String gender = generateGender(); + String name; + if (gender.equals(MALE)) { + name = generateMaleFirstName(); + } else { + name = generateFemaleFirstName(); + } + String surname = generateSurname(); + String company = generateCompany(); + + template = template.replace("@employee_id", "\"%s\"".formatted(generateEmployeeId())); + template = template.replace("@surname", "\"%s\"".formatted(surname)); + template = template.replace("@name", "\"%s\"".formatted(name)); + template = template.replace("@age", generateAge()); + template = template.replace("@ssn", "\"%s\"".formatted(generateSSN())); + template = template.replace("@hourly_rate", generateHourlyRate()); + template = template.replace("@gender", "\"%s\"".formatted(gender)); + template = template.replace("@email", "\"%s\"".formatted(generateEmail(name, surname, + generateEmailProvider()).toLowerCase(Locale.US))); + template = template.replace("@company", "\"%s\"".formatted(company)); + + return template; + } +} diff --git a/src/main/resources/data/companies.txt b/src/main/resources/data/companies.txt new file mode 100644 index 0000000..e2b6088 --- /dev/null +++ b/src/main/resources/data/companies.txt @@ -0,0 +1,20 @@ +Google +Microsoft +Apple +Amazon +Facebook +Tesla +IBM +Intel +Samsung +Toyota +General Electric +BMW +Sony +Cisco +Oracle +Nike +Coca-Cola +PepsiCo +Unilever +Johnson & Johnson diff --git a/src/main/resources/data/iot_vendors.txt b/src/main/resources/data/iot_vendors.txt new file mode 100644 index 0000000..483d8b7 --- /dev/null +++ b/src/main/resources/data/iot_vendors.txt @@ -0,0 +1,10 @@ +ikea +apple +tasmota +sencor +amazon +apple +xiaomi +samsung +panasonic +parkside diff --git a/src/main/resources/data/mail_provider.txt b/src/main/resources/data/mail_provider.txt new file mode 100644 index 0000000..83164b7 --- /dev/null +++ b/src/main/resources/data/mail_provider.txt @@ -0,0 +1,6 @@ +email.com +seznam.cz +gmail.com +outlook.com +yahoo.com +icloud.com diff --git a/src/main/resources/data/nameF.txt b/src/main/resources/data/nameF.txt new file mode 100644 index 0000000..b489432 --- /dev/null +++ b/src/main/resources/data/nameF.txt @@ -0,0 +1,50 @@ +Maria +Isabella +Fatima +Hannah +Sofia +Aisha +Mei +Elena +Moana +Sakura +Natalia +Irina +Amina +Greta +Aria +Marta +Lila +Marwa +Zara +Pooja +Yara +Katya +Carmen +Noriko +Alina +Misaki +Layla +Ksenia +Maya +Zainab +Noor +Aaliyah +Mia +Ewa +Chihiro +Irena +Fumiko +Ludmila +Chloe +Zoya +Eva +Siti +Priya +Suresh +Thiago +Greta +Aya +Misaki +Fumiko +Ewa diff --git a/src/main/resources/data/nameM.txt b/src/main/resources/data/nameM.txt new file mode 100644 index 0000000..580940b --- /dev/null +++ b/src/main/resources/data/nameM.txt @@ -0,0 +1,50 @@ +John +Hiroshi +Ahmed +Li +Vladimir +Pedro +Sergei +Antonio +Yuki +Carlos +Ivan +Alexei +Miguel +Jing +Omar +Haruto +Rahul +Abdullah +Juan +Igor +Bao +Diego +Kofi +Kenji +Abdul +Yu +Nikola +Stefan +Niko +Dimitri +Subhash +Katya +Jorge +Mahmud +Faisal +Joseph +Marco +Leandro +Samir +Matteo +Valentin +Jean +Franz +Bashir +Musa +Saif +Rashid +Olaf +Ali +Simran diff --git a/src/main/resources/data/surnames.txt b/src/main/resources/data/surnames.txt new file mode 100644 index 0000000..49d02a6 --- /dev/null +++ b/src/main/resources/data/surnames.txt @@ -0,0 +1,100 @@ +Smith +Muller +Kim +Skywalker +Wang +Johnson +Santos +Lopez +Brown +Patel +Li +Gonzalez +Nguyen +Martin +Rodriguez +Perez +Lee +Silva +Ahmed +Kowalski +Schmidt +Singh +Costa +Ivanov +Taylor +Hoxha +Davis +Chen +Rossi +Kimura +Torres +Sousa +Tan +Smith +Hernandez +Bashir +Johnson +Robinson +Lopez +Brown +Patel +Wilson +Garcia +Singh +Lee +Chen +Thompson +Evans +Harris +Baker +Carter +Murphy +Walker +Martinez +Diaz +Roberts +Jones +Sanchez +Kelly +Wright +Adams +Rivera +Mitchell +Young +Price +Bell +Scott +Torres +Morris +Reed +Cook +Howard +Brooks +Morgan +Bailey +Reed +Rivera +Cooper +Richardson +Cox +Ward +Gray +James +Watson +Perry +Russell +Foster +Powell +Long +Patterson +Hughes +Flores +Washington +Butler +Simmons +Foster +Gonzales +Bryant +Alexander +Russell diff --git a/src/main/resources/log4j2.properties b/src/main/resources/log4j2.properties new file mode 100644 index 0000000..ea5af2c --- /dev/null +++ b/src/main/resources/log4j2.properties @@ -0,0 +1,28 @@ +name = TFConfig + +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss}{GMT} [%thread] %highlight{%-5p} [%c{1}:%L] %m%n + +appender.rolling.type = RollingFile +appender.rolling.name = RollingFile +appender.rolling.fileName = ${env:TEST_LOG_DIR:-target/logs}/load-generator-debug-${env:BUILD_ID:-0}.log +appender.rolling.filePattern = ${env:TEST_LOG_DIR:-target/logs}/load-generator-debug-%d{yyyy-MM-dd-HH-mm-ss}-%i.log.gz +appender.rolling.policies.type = Policies +appender.rolling.policies.size.type = SizeBasedTriggeringPolicy +appender.rolling.policies.size.size=100MB +appender.rolling.strategy.type = DefaultRolloverStrategy +appender.rolling.strategy.max = 5 +appender.rolling.layout.type = PatternLayout +appender.rolling.layout.pattern=%d{yyyy-MM-dd HH:mm:ss}{GMT} %-5p [%c{1}:%L] %m%n + +rootLogger.level = ${env:TEST_ROOT_LOG_LEVEL:-DEBUG} +rootLogger.appenderRef.console.ref = STDOUT +rootLogger.appenderRef.console.level = ${env:TEST_LOG_LEVEL:-INFO} +rootLogger.appenderRef.rolling.ref = RollingFile +rootLogger.appenderRef.rolling.level = DEBUG +rootLogger.additivity = false + +logger.fabric8.name = io.fabric8.kubernetes.client +logger.fabric8.level = OFF diff --git a/src/main/resources/templates/iot_device.json b/src/main/resources/templates/iot_device.json new file mode 100644 index 0000000..375deaa --- /dev/null +++ b/src/main/resources/templates/iot_device.json @@ -0,0 +1,9 @@ +{ + "IPV4": @ipv4, + "MAC": @mac, + "ID": @id, + "TYPE": @type, + "LAST_UPDATE": @last_update, + "LINK_QUALITY": @link_quality, + "DATA": @data +} diff --git a/src/main/resources/templates/payroll_employee.json b/src/main/resources/templates/payroll_employee.json new file mode 100644 index 0000000..dd2e852 --- /dev/null +++ b/src/main/resources/templates/payroll_employee.json @@ -0,0 +1,11 @@ +{ + "employee_id": @employee_id, + "first_name": @name, + "last_name": @surname, + "age": @age, + "ssn": @ssn, + "hourly_rate": @hourly_rate, + "gender": @gender, + "email": @email, + "company": @company +} diff --git a/src/test/java/io/skodjob/loadgenerator/DataGeneratorTest.java b/src/test/java/io/skodjob/loadgenerator/DataGeneratorTest.java new file mode 100644 index 0000000..48bcbaf --- /dev/null +++ b/src/test/java/io/skodjob/loadgenerator/DataGeneratorTest.java @@ -0,0 +1,103 @@ +/* + * Copyright Skodjob authors. + * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html). + */ +package io.skodjob.loadgenerator; + +import com.fasterxml.jackson.databind.JsonNode; +import io.skodjob.loadgenerator.enums.ETemplateType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +class DataGeneratorTest { + private DataGenerator payrollDataGenerator; + private DataGenerator iotDataGenerator; + + @BeforeEach + void setUp() { + payrollDataGenerator = new DataGenerator(ETemplateType.PAYROLL_EMPLOYEE); + iotDataGenerator = new DataGenerator(ETemplateType.IOT_DEVICE); + } + + @ParameterizedTest(name = "{0}") + @MethodSource("testRepeatedParameters") + void testValidateGeneratedData(String testName, String regex, DataGenerator generator) { + Pattern dataPattern = Pattern.compile(regex); + + String data = generator.generateStringData(); + assertNotNull(data, "Generated string data should not be null for PAYROLL_EMPLOYEE"); + assertFalse(data.contains("\"@"), "Generated string data should not contain placeholders for PAYROLL_EMPLOYEE"); + + Matcher matcher = dataPattern.matcher(data); + assertTrue(matcher.matches(), "Generated data doesn't match the expected format!"); + } + + @Test + void testGenerateJsonDataPayroll() throws IOException { + JsonNode jsonData = payrollDataGenerator.generateJsonData(); + assertNotNull(jsonData, "Generated JSON data should not be null for PAYROLL_EMPLOYEE"); + assertTrue(jsonData.isObject(), "Generated JSON data should be a JSON object for PAYROLL_EMPLOYEE"); + } + + @Test + void testGenerateJsonDataIot() throws IOException { + JsonNode jsonData = iotDataGenerator.generateJsonData(); + assertNotNull(jsonData, "Generated JSON data should not be null for IOT_DEVICE"); + assertTrue(jsonData.isObject(), "Generated JSON data should be a JSON object for IOT_DEVICE"); + } + + @Test + void testInvalidTemplateType() { + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + DataGenerator invalidDataGenerator = new DataGenerator(null); + invalidDataGenerator.generateStringData(); + }, "Expected generateStringData() to throw an exception, but it didn't"); + + assertTrue(thrown.getMessage().contains("TemplateType cannot be null!"), "Exception message should contain 'TemplateType cannot be null!'"); + } + + public static Stream testRepeatedParameters() { + String peopleRegex = "\\{" + + "\"employee_id\":\"\\d+\"," + + "\"first_name\":\"[A-Za-z]+\"," + + "\"last_name\":\"[A-Za-z]+\"," + + "\"age\":\\d+," + + "\"ssn\":\"\\d+-\\d+-\\d+\"," + + "\"hourly_rate\":\\d+\\.\\d+," + + "\"gender\":\"(Male|Female)\"," + + "\"email\":\"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\"," + + "\"company\":\"[A-Za-z-& ]+\"" + + "\\}"; + + String iotRegex = "\\{" + + "\"IPV4\":\"\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\"," + + "\"MAC\":\"([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}\"," + + "\"ID\":\"\\d+\"," + + "\"TYPE\":\"(light|button|thermometer|plug|custom|gate)\"," + + "\"LAST_UPDATE\":\"\\d+\"," + + "\"LINK_QUALITY\":\\d{1,3}," + + "\"DATA\":\\{.*\\}" + + "\\}"; + + + return IntStream.range(0, 10000) + .mapToObj(i -> Stream.of( + Arguments.of(ETemplateType.PAYROLL_EMPLOYEE.getTemplateName(), peopleRegex, + new DataGenerator(ETemplateType.PAYROLL_EMPLOYEE)), + Arguments.of(ETemplateType.IOT_DEVICE.getTemplateName(), iotRegex, + new DataGenerator(ETemplateType.IOT_DEVICE)) + )) + .flatMap(stream -> stream); + } +}