Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First commit

  • Loading branch information...
commit e8cfd69c4f9e1c68c3b4af36d192b7668200e56b 0 parents
cap_protect authored
3  .gitignore
@@ -0,0 +1,3 @@
+.idea
+target
+loadtest.iml
6 Example1.groovy
@@ -0,0 +1,6 @@
+// run: java -jar loadtest.jar -n 10 -d 1000 Example1.groovy
+try {
+ HTTP.get("http://google.com", "http://facebook.com");
+} catch (Throwable thr) {
+ System.err.println(thr);
+}
1  README
@@ -0,0 +1 @@
+This file was created by IntelliJ IDEA 10.5.2 for binding GitHub repository
21 README.textile
@@ -0,0 +1,21 @@
+h1. Web load test tool v1.0
+
+Tool could be used to test load your web application using different scenarios. Scripts are written in groovy using special test environment.
+
+h2. Run
+
+Copy one loadtest-1.0.jar and Example*.groovy from repository to one of directories on your computer. Use java to run it from commandline:
+
+java -jar loadtest-1.0.jar Example1.groovy
+
+h2. Environment
+
+In groovy following classes avialable: HTTP and Variations.
+
+h2. License
+
+Load test tool is distributed on terms of BSD-license. Look at src/main/resources/BSD-LICENSE.txt for details.
+
+Groovy used internally by load test tool is licensed under the Apache 2 license:
+
+http://www.apache.org/licenses/LICENSE-2.0.html
BIN  loadtest-1.0.jar
Binary file not shown
69 pom.xml
@@ -0,0 +1,69 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>org.loadtest</groupId>
+ <artifactId>loadtest</artifactId>
+ <packaging>jar</packaging>
+ <version>1.0</version>
+ <name>Web load test</name>
+ <url>http://maven.apache.org</url>
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ </properties>
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.groovy</groupId>
+ <artifactId>groovy</artifactId>
+ <version>1.8.3</version>
+ </dependency>
+ <dependency>
+ <groupId>args4j</groupId>
+ <artifactId>args4j</artifactId>
+ <version>2.0.16</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <version>2.3</version>
+ <executions>
+ <execution>
+ <id>unpack</id>
+ <phase>prepare-package</phase>
+ <goals>
+ <goal>unpack-dependencies</goal>
+ </goals>
+ <configuration>
+ <excludes>META-INF/MANIFEST.MF,META-INF/LICENSE.txt,META-INF/maven/**</excludes>
+ <outputDirectory>${project.build.outputDirectory}</outputDirectory>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <version>2.3.1</version>
+ <configuration>
+ <archive>
+ <index>true</index>
+ <manifest>
+ <mainClass>org.loadtest.LoadTest</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>versions-maven-plugin</artifactId>
+ <version>1.2</version>
+ <configuration>
+ <newVersion>${parsedVersion.majorVersion}.${parsedVersion.minorVersion}</newVersion>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
28 src/main/java/org/loadtest/Globals.java
@@ -0,0 +1,28 @@
+package org.loadtest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Object used by scripts to interchange values
+ */
+public class Globals {
+ private Map values = new HashMap();
+
+ public synchronized void put(String name, Object value) {
+ values.put(name, value);
+ }
+
+ public synchronized Object get(String name) {
+ return values.get(name);
+ }
+ public synchronized long increment(String name) {
+ Number num = (Number)values.get(name);
+ if (num == null) {
+ num = 0L;
+ }
+ long res = num.longValue();
+ values.put(name, res + 1);
+ return res;
+ }
+}
141 src/main/java/org/loadtest/LoadTest.java
@@ -0,0 +1,141 @@
+package org.loadtest;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tool to run test load specified by script using several threads.
+ * Main entry point.
+ */
+public class LoadTest {
+ private Options options;
+ private ScheduledExecutorService executor;
+ private ScheduledExecutorService printExecutor;
+ private ScriptRunner scriptRunner;
+
+ public LoadTest(int threads, long delay, File script) {
+ this.options = new Options(threads, delay, script);
+ }
+ public LoadTest(Options options) {
+ this.options = options;
+ }
+
+ public synchronized void start() {
+ if (executor != null) {
+ return;
+ }
+ scriptRunner = new ScriptRunner(this, options.scripts);
+ executor = Executors.newScheduledThreadPool(options.threads);
+ for (int i = 0; i < options.threads; i++) {
+ executor.scheduleWithFixedDelay(scriptRunner,
+ options.delay * i / options.threads,
+ options.delay,
+ TimeUnit.MILLISECONDS);
+ }
+
+ printExecutor = Executors.newSingleThreadScheduledExecutor();
+ printExecutor.scheduleWithFixedDelay(new StatsPrinter(),
+ 500L,
+ 1000L,
+ TimeUnit.MILLISECONDS);
+ }
+
+ public synchronized void stop() {
+ if (executor == null) { return; }
+ executor.shutdownNow();
+ executor = null;
+ printExecutor.shutdownNow();
+ printExecutor = null;
+ scriptRunner = null;
+ }
+
+ public boolean isStopped() {
+ return executor == null;
+ }
+
+ public static void main(String[] args) throws IOException {
+ LoadTest testload = new LoadTest(new Options().parse(args));
+ Scanner scanner = new Scanner(System.in);
+ try {
+ do {
+ System.out.println("Starting(press q[ENTER] - to quit, [ENTER] - to stop)");
+ testload.start();
+ if (scanner.nextLine().equals("q")) {
+ break;
+ }
+ System.out.println("Stopping(press q[ENTER] - to quit, [ENTER] - to start)");
+ testload.stop();
+ } while (!scanner.nextLine().equals("q"));
+ } catch (Throwable thr) {
+ System.out.println("Breaked");
+ }
+ }
+
+ public synchronized ScriptRunner getScriptRunner() {
+ return scriptRunner;
+ }
+
+ private static class Options {
+ @Option(name="-n", usage="number of threads")
+ private int threads = 1;
+
+ @Option(name="-d", usage="delay between requests")
+ private long delay = 1;
+
+ @Option(name="-s", usage="number of the slowest queries to show in statistics")
+ private int slowQueriesToShow = 24;
+
+ @Argument
+ private List<File> scripts = new ArrayList();
+
+ public Options(int threads, long delay, File script) {
+ this.threads = threads;
+ this.delay = delay;
+ this.scripts.add(script);
+ }
+
+ public Options() {
+ }
+
+ public Options parse(String[] args) {
+ CmdLineParser parser = new CmdLineParser(this);
+ try {
+ parser.parseArgument(args);
+ if (scripts.isEmpty()) {
+ usage(parser, new CmdLineException(parser, "specify one or more scripts"));
+ }
+ } catch (CmdLineException e) {
+ usage(parser, e);
+ }
+ return this;
+ }
+
+ private void usage(CmdLineParser parser, CmdLineException e) {
+ System.err.println(e.getMessage());
+ System.err.println("java -jar loadtest.jar [options...] scripts...");
+ parser.printUsage(System.err);
+ System.exit(1);
+ }
+ }
+
+ private class StatsPrinter implements Runnable {
+ public void run() {
+ ScriptRunner runner = getScriptRunner();
+ if (runner != null) {
+ runner.getStats().report(options.slowQueriesToShow);
+ }
+ }
+ }
+
+}
74 src/main/java/org/loadtest/ScriptRunner.java
@@ -0,0 +1,74 @@
+package org.loadtest;
+
+import groovy.lang.Binding;
+import groovy.lang.GroovyCodeSource;
+import groovy.lang.GroovyShell;
+import org.codehaus.groovy.control.CompilationFailedException;
+
+import java.io.File;
+import java.util.List;
+import java.util.Random;
+
+/**
+ * Runs scripts in environment produced by "Classes.groovy" and three bindings:
+ * GLOBAL - global variables interchanger,
+ * STATS - statistics object,
+ * RANDOM - random object
+ */
+public class ScriptRunner implements Runnable {
+ private LoadTest load;
+ private List scripts;
+ private Stats stats = new Stats();
+ private Globals globals = new Globals();
+ private Random random = new Random();
+ private static ThreadLocal local = new ThreadLocal();
+
+ public ScriptRunner(LoadTest load, List scripts) {
+ this.load = load;
+ this.scripts = scripts;
+ }
+
+ protected Binding createNewBinding() {
+ Binding binding = new Binding();
+ binding.setVariable("STATS", stats);
+ binding.setVariable("GLOBALS", globals);
+ binding.setVariable("RANDOM", random);
+ return binding;
+ }
+
+ public void run() {
+ Binding binding = createNewBinding();
+ GroovyShell shell = new GroovyShell(binding);
+ stats.addRun();
+ try {
+ local.set(shell);
+ GroovyCodeSource functions = new GroovyCodeSource(ScriptRunner.class.getResource("Classes.groovy"));
+ shell.evaluate(functions);
+ for (Object script : scripts) {
+ shell.evaluate((File)script);
+ }
+ } catch (CompilationFailedException e) {
+ stats.addError();
+ errorCase(e);
+ } catch (Throwable e) {
+ stats.addError();
+ } finally {
+ local.remove();
+ }
+ }
+
+ private void errorCase(Exception e) {
+ if (!load.isStopped()) {
+ e.printStackTrace();
+ load.stop();
+ }
+ }
+
+ public Stats getStats() {
+ return stats;
+ }
+
+ public static GroovyShell getGroovyShell() {
+ return (GroovyShell) local.get();
+ }
+}
105 src/main/java/org/loadtest/Stats.java
@@ -0,0 +1,105 @@
+package org.loadtest;
+
+import java.util.*;
+
+/**
+ * Class accumulates statistics: number of runs, errors, queries and http fetch execution time.
+ */
+public class Stats {
+ public Stats() {
+ reset();
+ }
+
+ public synchronized void reset() {
+ runs = 0;
+ queries = 0;
+ errors = 0;
+ urlStats = new TreeMap();
+ }
+
+ private int runs;
+ private int queries;
+ private int errors;
+ private Map urlStats;
+
+ private static class UrlStat {
+ private int count;
+ private long time;
+ private long minimum;
+ private long maximum;
+ private final String url;
+
+ private UrlStat(String url) {
+ this.url = url;
+ }
+
+
+ public void add(long time) {
+ if (count == 0 || minimum > time) {
+ minimum = time;
+ }
+ if (count == 0 || maximum > time) {
+ maximum = time;
+ }
+ this.time += time;
+ this.count++;
+ }
+
+ public double getAvarage() {
+ if (count == 0) {
+ return 0;
+ } else {
+ return (double)time / count;
+ }
+ }
+
+ public long getMinimum() {
+ return minimum;
+ }
+
+ public long getMaximum() {
+ return maximum;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+ }
+
+ public synchronized void addRun() { runs++; }
+ public synchronized void addQuery(String url, long time) {
+ queries++;
+ UrlStat stat = (UrlStat) urlStats.get(url);
+ if (stat == null) {
+ stat = new UrlStat(url);
+ urlStats.put(url, stat);
+ }
+ stat.add(time);
+ }
+ public synchronized void addError() { errors++; }
+
+ public synchronized void report(int slowQueriesToShow) {
+ System.out.println("Stats{" +
+ "runs=" + runs +
+ ", queries=" + queries +
+ ", errors=" + errors +
+ '}');
+ List lst = new ArrayList(urlStats.values());
+ Collections.sort(lst, new UrlStatComparator());
+ for (Object stat : lst.subList(0, Math.min(slowQueriesToShow, lst.size()))) {
+ UrlStat urlStat = (UrlStat) stat;
+ System.out.printf("min=%5d avg=%8.2f max=%5d %s%n",
+ urlStat.getMinimum(),
+ urlStat.getAvarage(),
+ urlStat.getMaximum(),
+ urlStat.getUrl());
+
+ }
+ }
+
+ private class UrlStatComparator implements Comparator<UrlStat> {
+ public int compare(UrlStat o1, UrlStat o2) {
+ return Double.compare(o1.getAvarage(), o2.getAvarage());
+ }
+ }
+}
23 src/main/resources/BSD-LICENSE.txt
@@ -0,0 +1,23 @@
+Copyright (c) 2011, captain.protect@gmail.com
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
+OF SUCH DAMAGE.
110 src/main/resources/org/loadtest/Classes.groovy
@@ -0,0 +1,110 @@
+import java.util.regex.Pattern
+import java.util.regex.Matcher
+import org.loadtest.ScriptRunner
+
+
+class Variations{
+ private static def getBindings() {
+ return ScriptRunner.getGroovyShell().getContext();
+ }
+
+ public static def select(...args) {
+ double v = 0;
+ for (int i = 0; i < args.length; i += 2) {
+ v += args[i];
+ args[i] = v;
+ }
+ for (int i = 0; i < args.length; i += 2) {
+ args[i] /= v;
+ }
+ double coinExperiment = getBindings().RANDOM.nextDouble(1.0);
+ for (int i = 0; i < args.length; i += 2) {
+ if (coinExperiment < args[i]) {
+ args[i+1]();
+ break;
+ }
+ }
+ }
+
+ public static def any(Object ...args) {
+ return args[getBindings().RANDOM.nextInt(args.length)];
+ }
+
+ public static void main(String[] args) {}
+}
+
+class HTTP {
+ private static def getBindings() {
+ return ScriptRunner.getGroovyShell().getContext();
+ }
+
+ public static def get(Closure reporter, String ...urls) {
+ long start = System.currentTimeMillis();
+ String urlString = Variations.any(urls);
+ URL url = new URL(urlString);
+ URLConnection conn = url.openConnection();
+ String contentType = conn.getContentType();
+ String charset = "UTF-8";
+ if (contentType != null && !contentType.isEmpty()) {
+ Matcher matcher = Pattern.compile("charset=([^ ]+)", Pattern.CASE_INSENSITIVE)
+ .matcher(contentType);
+ if (matcher.find()) {
+ charset = matcher.group(1);
+ }
+ }
+ InputStream input = url.openStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(input, charset));
+ String line;
+ Object result = null;
+ while ((line = reader.readLine()) != null) {
+ if (reporter != null) {
+ result = reporter(line);
+ }
+ }
+ reader.close();
+ long end = System.currentTimeMillis();
+ getBindings().STATS.addQuery(urlString, end - start);
+ return result;
+ }
+
+ public static def get(String ...urls) {
+ return get(null, urls);
+ }
+
+ public static def parse(Pattern regex, String ...html) {
+ List result = new ArrayList();
+ for (String line : html) {
+ Matcher matcher = regex.matcher(line);
+ while (matcher.find()) {
+ String []res = new String[matcher.groupCount()];
+ for (int i = 1; i <= matcher.groupCount(); i++) {
+ res[i - 1] = matcher.group(i);
+ }
+ result.add(res);
+ }
+ }
+ boolean oneSizer = true;;
+ for (String []arr : result) {
+ if (arr.length != 1) oneSizer = false;
+ }
+ if (oneSizer) {
+ String []resultArray = new String[result.size()];
+ int i = 0;
+ for (String []arr : result) {
+ resultArray[i++] = arr[0];
+ }
+ return resultArray;
+ } else {
+ String [][]resultArray = result.toArray(new String[result.size()][0]);
+ return resultArray;
+ }
+ }
+
+ public static def resolve(String uri, String path) {
+ return new URI(uri).resolve(path).toString();
+ }
+
+ public static void main(String[] args) {}
+}
+
+
6 src/main/resources/org/loadtest/Vars.groovy
@@ -0,0 +1,6 @@
+import org.loadtest.Globals
+import org.loadtest.Stats
+
+Globals GLOBALS;
+Stats STATS;
+Random RANDOM;
Please sign in to comment.
Something went wrong with that request. Please try again.