From 777309a71ec69f264f90600bb03da251dd274cba Mon Sep 17 00:00:00 2001 From: jiahuili Date: Tue, 21 Sep 2021 11:44:21 -0500 Subject: [PATCH] Tests Using Locust and Faker --- pom.xml | 29 +++++++++ test/README.md | 51 +++++++++++++++ test/data.py | 41 ++++++++++++ test/locustfile.py | 156 +++++++++++++++++++++++++++++++++++++++++++++ test/run | 29 +++++++++ 5 files changed, 306 insertions(+) create mode 100644 test/README.md create mode 100644 test/data.py create mode 100644 test/locustfile.py create mode 100755 test/run diff --git a/pom.xml b/pom.xml index 338e398a..f560b141 100644 --- a/pom.xml +++ b/pom.xml @@ -148,6 +148,15 @@ ${scala.plugin.version} + + clouseau + com.cloudant.clouseau.Main + + -Dclouseau.name=clouseau@127.0.0.1 + -Dclouseau.cookie=monster + -Dclouseau.dir=${basedir}/target/clouseau + + clouseau1 com.cloudant.clouseau.Main @@ -346,6 +355,26 @@ + + + org.jacoco + jacoco-maven-plugin + 0.7.9 + + + + prepare-agent + + + + generate-code-coverage-report + test + + report + + + + diff --git a/test/README.md b/test/README.md new file mode 100644 index 00000000..5ed589d8 --- /dev/null +++ b/test/README.md @@ -0,0 +1,51 @@ +# Locust Test + +Test `Clouseau` using [Locust](https://github.com/locustio/locust) and [Faker](https://github.com/joke2k/faker). + +## Configuration options + +Locust configuration options. + +Command line | Description +--- | --- +--headless | Disable the web interface, and start the test +--only-summary | Only print the summary stats +--host | Host to load test +-u | Peak number of concurrent Locust users +-r | Rate to spawn users at (users per second) +-t | Stop after the specified amount of time +--docs-number | The number of generated documents (default: 10) + +``` +locust -f locustfile.py --headless --only-summary --docs-number 10 -u 1 -r 1 -t 10 +``` + +## Basic Usage + +Run `CouchDB` and `Clouseau` in different terminals, and then run the locust test: + +``` +# Open 4 different terminals and run the command: +./dev/run --admin=adm:pass +mvn scala:run -Dlauncher=clouseau1 +mvn scala:run -Dlauncher=clouseau2 +mvn scala:run -Dlauncher=clouseau3 +``` + +### Install dependencies: + +``` +./run install +``` + +### Run random_tree_generator tests: + +``` +./run locust +``` + +### Cleanup + +``` +./run clean +``` diff --git a/test/data.py b/test/data.py new file mode 100644 index 00000000..01a173b0 --- /dev/null +++ b/test/data.py @@ -0,0 +1,41 @@ +import json +from datetime import date +from random import choice +from faker import Faker + + +def write_to_files(data, filename): + with open(filename, 'w') as outfile: + json.dump(data, outfile) + + +def gen_data(n=10): + data = [] + counter = {} + fake = Faker() + fields = ['married', 'ethnicity', 'gender'] + counter['total_rows'] = n + + for i in range(n): + data.append({'_id': str(i)}) + data[i]['gender'] = choice(['M', 'F']) + data[i]['name'] = fake.name_male() if data[i]['gender'] == 'M' else fake.name_female() + data[i]['date_of_birth'] = fake.iso8601() + data[i]['age'] = date.today().year - int(data[i]['date_of_birth'][:4]) + data[i]['married'] = 'False' if data[i]['age'] < 22 else choice(['True', 'False']) + data[i]['ethnicity'] = choice(['White', 'Black', 'Asian', 'Hispanic', 'non-Hispanic']) + data[i]['address'] = {'full_address': fake.address()} + data[i]['address']['city'] = data[i]['address']['full_address'][ + data[i]['address']['full_address'].find('\n') + 1: -10] + data[i]['address']['area'] = data[i]['address']['full_address'][-8:-6] + data[i]['address']['zip'] = data[i]['address']['full_address'][-5:] + data[i]['lat'] = float(fake.latitude()) + data[i]['long'] = float(fake.longitude()) + + for field in fields: + if field not in counter: + counter[field] = {} + counter[field].update({data[i][field]: counter[field].get(data[i][field], 0) + 1}) + + write_to_files(data, 'data.json') + write_to_files(counter, 'analysis.json') diff --git a/test/locustfile.py b/test/locustfile.py new file mode 100644 index 00000000..122b3973 --- /dev/null +++ b/test/locustfile.py @@ -0,0 +1,156 @@ +import json +import requests +import data + +from locust import events, HttpUser, constant, task, SequentialTaskSet, tag + +URL = 'http://adm:pass@localhost:15984' +DB = 'http://adm:pass@localhost:15984/demo' +SESSION = requests.session() + + +def create_database(): + if SESSION.get(DB).status_code == 200: + SESSION.delete(DB) + SESSION.put(DB) + + +def insert_docs(): + payload = {"docs": []} + with open('data.json') as json_file: + payload['docs'].extend(json.load(json_file)) + SESSION.post(DB + '/_bulk_docs', json=payload, headers={"Content-Type": "application/json"}) + + +def create_indexes(): + design_docs = { + "_id": "_design/search", + "indexes": { + "search_index": { + "index": "function(doc) {if(doc.gender) {index(\"gender\", doc.gender, {\"store\": true} );};" + "if(doc.age) {index(\"age\", doc.age, {\"store\": true} );};" + "if(doc.married) {index(\"married\", doc.married, {\"store\": true} );};" + "if(doc.ethnicity) {index(\"ethnicity\", doc.ethnicity, {\"store\": true} );}}" + } + } + } + SESSION.put(f'{DB}/_design/search', data=json.dumps(design_docs)) + + +def get_result(condition, response, func_name): + response.success() if condition else response.failure(func_name + ' FAILED.') + + +@events.init_command_line_parser.add_listener +def _(parser): + parser.add_argument("--docs-number", type=int, env_var="LOCUST_DOCS_NUMBER", default=5, + help="How many documents do you want to generate") + + +@events.test_start.add_listener +def _(environment, **kw): + print('1. Generate documents') + data.gen_data(environment.parsed_options.docs_number) + + +class CouchDBTest(SequentialTaskSet): + def on_start(self): + self.client.get('/', name=self.on_start.__name__) + print('2. Create Database, Insert docs and _design docs') + create_database() + insert_docs() + create_indexes() + print('3. Start testing ... ') + with open('analysis.json') as json_file: + self.data = json.load(json_file) + + @tag('get') + @task + def get_all_dbs(self): + with self.client.get('/demo/_all_dbs', catch_response=True, name='Get All DBs') as response: + get_result( + len(response.text) and response.elapsed.total_seconds() < 2.0, + response, self.get_all_dbs.__name__) + + @tag('get') + @task + def get_all_docs(self): + with self.client.get('/demo/_all_docs', catch_response=True, name='Get All Docs') as response: + get_result( + len(response.text) and response.elapsed.total_seconds() < 2.0, + response, self.get_all_docs.__name__) + + @tag('search') + @task + def search_all_docs(self): + with self.client.get('/demo/_design/search/_search/search_index?query=*:*', + catch_response=True, name='Search All Docs') as response: + get_result( + response.status_code == 200 and response.json()['total_rows'] == self.data['total_rows'], + response, self.search_all_docs.__name__) + + @tag('search') + @task + def search_gender_is_male(self): + with self.client.get('/demo/_design/search/_search/search_index?query=gender:m', + catch_response=True, name='Search Gender is Male') as response: + get_result( + response.status_code == 200 and response.json()['total_rows'] == self.data['gender']['M'], + response, self.search_gender_is_male.__name__) + + @tag('search') + @task + def search_gender_is_male_with_limit_2(self): + with self.client.get('/demo/_design/search/_search/search_index?query=gender:m&limit=2', + catch_response=True, name='Search Gender is Male with Limit 2') as response: + get_result( + response.status_code == 200 and len(response.json()['rows']) == 2, + response, self.search_gender_is_male_with_limit_2.__name__) + + @tag('search') + @task + def search_gender_is_female_and_sort_by_age(self): + with self.client.get('/demo/_design/search/_search/search_index?query=gender:f&sort="age"', + catch_response=True, name='Search Gender is Female AND Sort by age') as response: + result = response.json() + if self.data['gender']['F'] >= 2: + conditions = result['total_rows'] == self.data['gender']['F'] and \ + result['rows'][0]['fields']['age'] <= result['rows'][1]['fields']['age'] + else: + conditions = result['total_rows'] == self.data['gender']['F'] + get_result(conditions, response, self.search_gender_is_female_and_sort_by_age.__name__) + + @tag('search') + @task + def search_married_people_age_should_greater_than_21(self): + with self.client.get( + '/demo/_design/search/_search/search_index?query=married:true', + catch_response=True, name='Search married people age > 21') as response: + result = response.json() + for i in result['rows']: + if i['fields']['age'] <= 21: + response.failure(self.search_married_people_age_should_greater_than_21.__name__) + response.success() + + @tag('search') + @task + def search_ethnicity_White_OR_Asian(self): + with self.client.get( + f'/demo/_design/search/_search/search_index?query=ethnicity:White OR ethnicity:Asian', + catch_response=True, name='Search ethnicity White OR Asian') as response: + result = response.json() + get_result( + response.status_code == 200 and + result['total_rows'] == self.data['ethnicity']['White'] + self.data['ethnicity']['Asian'], + response, self.search_ethnicity_White_OR_Asian.__name__) + + def on_stop(self): + self.client.get('/', name=self.on_stop.__name__) + print("4. Delete database, and shut down the locust") + SESSION.delete(DB) + + +class LoadTest(HttpUser): + host = URL + wait_time = constant(1) + tasks = [CouchDBTest] diff --git a/test/run b/test/run new file mode 100755 index 00000000..31d90de2 --- /dev/null +++ b/test/run @@ -0,0 +1,29 @@ +#!/bin/bash +case $1 in +h | help) + echo "Common options: + h, help Show this help message + i, install Install dependencies + l, locust Run locust tests + (Specify the docs number to be tested, run $./run l {docs_number}}) + c, cleanup Cleanup the directory" + ;; +i | install) + echo Install dependencies + python3 -m pip install Faker locust + ;; +l | locust) + if [ $2 -ge 1 ]; then + locust -f locustfile.py --headless --only-summary --docs-number $2 -u 1 -r 1 -t 600 + else + locust -f locustfile.py --headless --only-summary --docs-number 10 -u 1 -r 1 -t 10 + fi + ;; +c | clean) + rm -rf analysis.json data.json + echo cleanup DONE + ;; +*) + echo don\'t know + ;; +esac