1
0
mirror of https://github.com/gluster/glusterfs.git synced 2026-02-06 09:48:44 +01:00
Files
glusterfs/tools/tests/regression
Xavi Hernandez 1278939a03 tests: new testing tool
A new tool gftest has been created to automate and simplify the
preparation of an environment to run tests. It can also process
the full set of regression tests in parallel to significantly
reduce the total execution time.

It's written in python and requires these components:

- libvirt
- qemu-kvm
- gnupg
- python-click

All other modules should already be present in a default installation.

Using this tool is quite simple:

1. Create the VM

```
gftest create [OPTIONS] <name>

Options:
  --template <name> Template to use (default: centos7)
  --port <num>      Port to access the VM (default: 2222)
  --key <path>      Private key for SSH (default: ~/.ssh/id_rsa)
  --cpus <num>      Number of virtual cores of the VM
  --memory <num>    Memory assigned to the VM in GiB
  --disk <num>      Disk space for the VM in GiB
```

This creates a KVM VM named <name> that can be accessed through the
specified port on the local host (i.e. `ssh -p <port> root@127.0.0.1`)
using the specified private key. All other option will be taken from
the template if not present.

This process also creates a container inside the VM with all the
required packages and configurations to build Gluster and run the
tests.

2. Build the code

```
gftest build [OPTIONS] <name>

Options:
  --commit <id>  SHA/name of the commit to compile
```

This copies the current git repo to the VM and compiles the specified
commit in a container image.

3. Start the workers

```
gftest spawn [OPTIONS] <name>

Options:
  --workers <num>  Number of workers to create
  --space <num>    Space allocated for running tests in GiB
```

This starts the specified number of container instances of the compiled
Gluster image.

4. Run the tests

```
gftest run <name> <output dir>
```

This runs the full set of tests in parallel in all available containers
and puts the results into the <output dir>.

It also collects statistics for each test and CPU and memory state
during the full run.

5. Access the VM or container for manual testing/debugging

```
gftest sh <name> [<idx>]
```

Without `idx` it opens a shell session to the VM. With an index it opens
a shell session to the idx-th testing container.

There are some additional commands:

- `gftest kill <name>`
  Stops and destroys all testing containers.

- `gftest shutdown <name>`
  Gracefully shuts down the VM.

- `gftest poweroff <name>`
  Forcibly stops the VM.

- `gftest poweron <name>`
  Starts the VM.

Updates: #3469
Change-Id: I169877a4c5197d001bf2822ad7116559f7d78754
Signed-off-by: Xavi Hernandez <xhernandez@redhat.com>
2022-09-30 12:51:17 +02:00

232 lines
7.3 KiB
Python
Executable File

#!/usr/bin/python3
import sys
import os
from multiprocessing import Process, Queue, Event
from threading import Thread
import time
import requests
import subprocess
import signal
from pathlib import Path
HOST_URL = os.environ.get('HOST_URL')
subprocess.run(['modprobe', 'dm_thin_pool', 'dm_snapshot'], check = True)
class TestStats(object):
def __init__(self):
self.data = {}
self.selected = []
def __enter__(self):
self.data = {}
self.selected = []
self.get()
return self
def __exit__(self, exc_type, exc_value, exc_bt):
self.put()
def __iter__(self):
self.selected.sort(key = lambda x: (-self.data[x]['avg'], x))
return iter(self.selected)
def get(self):
if HOST_URL is None:
return
res = requests.get(f'{HOST_URL}/run/tests.json')
if res.status_code == 200:
self.data = res.json()
def put(self):
if HOST_URL is not None:
data = {}
for name in self.data:
item = self.data[name]
if item['count'] > 0:
data[name] = item
requests.put(f'{HOST_URL}/run/tests.json', json = data)
def get_data(self, name):
if name not in self.data:
self.data[name] = { 'count': 0, 'elapsed': 0, 'retries': 0, 'avg': float('inf') }
return self.data[name]
def select(self, name):
self.get_data(name)
self.selected.append(name)
def add(self, name, elapsed, retries):
item = self.get_data(name)
item['count'] += 1
item['elapsed'] += elapsed
item['retries'] += retries
item['avg'] = item['elapsed'] / item['count']
class GlusterTesting(object):
def __init__(self, stats):
self.stats = stats
self.procs = []
self.requests = Queue()
self.results = Queue()
self.event = Event()
def __enter__(self):
proc = subprocess.run(['podman', 'ps', '--format', '{{.Names}}'], stdout=subprocess.PIPE, check = True, universal_newlines = True, encoding = 'utf-8')
containers = proc.stdout.splitlines()
for i in range(len(containers)):
proc = Process(target = self.worker, args = (containers[i].strip(),))
proc.start()
self.procs.append(proc)
self.event.clear()
self.monitor_thread = Thread(target = self.monitor)
self.monitor_thread.start()
self.collect_thread = Thread(target = self.collect)
self.collect_thread.start()
return self
def __exit__(self, exc_type, exc_value, exc_bt):
old = signal.signal(signal.SIGINT, signal.SIG_IGN)
for i in range(len(self.procs)):
self.process(None)
for proc in self.procs:
proc.join()
self.results.put(None)
self.collect_thread.join()
self.event.set()
self.monitor_thread.join()
signal.signal(signal.SIGINT, old)
def scan_loadavg(self):
with open('/proc/loadavg', 'r') as f:
data = f.readline().strip().split()[:4]
runnable, total = data[3].split('/')
return [float(data[0]), float(data[1]), float(data[2]), int(runnable), int(total)]
def scan_meminfo(self):
free = -1
avail = -1
count = 0
with open('/proc/meminfo', 'r') as f:
for line in f.readlines():
name, value = line.strip().split(':')
if name == 'MemFree':
free = int(value.strip().split()[0])
elif name == 'MemAvailable':
avail = int(value.strip().split()[0])
else:
continue
count += 1
if count >= 2:
break
return [free / 1024, avail / 1024]
def scan_stat(self):
with open('/proc/stat', 'r') as f:
for line in f.readlines():
data = line.strip().split()
if data[0] == 'cpu':
break
return [int(x) for x in data[1:9]]
def monitor(self):
with open('/tmp/monitor', 'w') as state:
hz = os.sysconf(os.sysconf_names['SC_CLK_TCK']) / 100
previous = self.scan_stat()
last = time.time()
while not(self.event.wait(1)):
now = time.time()
delay = (now - last) * hz
data = [f'{now:.6f}']
data.extend([f'{x:8.2f}' for x in self.scan_meminfo()])
load = self.scan_loadavg()
data.extend([f'{x:6.2f}' for x in load[:3]])
data.extend([f'{x:6d}' for x in load[3:]])
cpu = self.scan_stat()
data.extend([f'{(cpu[i] - previous[i]) / delay:6.2f}' for i in range(8)])
state.write(' '.join(data) + '\n')
last = now
previous = cpu
def collect(self):
start = time.time()
total = len(self.stats.selected)
count = 0
data = self.results.get()
while data is not None:
container = data[0]
name = data[1]
elapsed = data[2]
retries = data[3]
if retries < 0:
res = '31;1mFAILED'
retries = -retries
elif retries == 1:
res = '32;1mPASSED'
else:
res = '33;1mPASSED'
count += 1
runtime = (time.time() - start) / 60
msg = f"{count:4d}/{total} {runtime:5.1f} [{container}] {elapsed:6.1f} {retries} \x1b[{res}\x1b[0m {name}\n"
sys.stdout.write(msg)
self.stats.add(name, elapsed, retries)
data = self.results.get()
def process(self, name):
self.requests.put(name)
def worker(self, container):
try:
name = self.requests.get()
while name is not None:
elapsed = time.time()
retries = self.launch(container, name)
elapsed = time.time() - elapsed
self.results.put((container, name, elapsed, retries))
name = self.requests.get()
except KeyboardInterrupt:
pass
def launch(self, container, name):
cmd = ['podman', 'exec']
if HOST_URL is not None:
cmd.extend(['-e', f'HOST_URL={HOST_URL}'])
cmd.extend([container, '/root/glusterfs/tools/tests/prove_run', name])
for i in range(2):
proc = subprocess.run(cmd, stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, check = False, universal_newlines = True, encoding = 'utf-8')
if proc.returncode == 0:
return i + 1
return -2
if __name__ == "__main__":
with TestStats() as tests:
cmd = [str(Path(sys.argv[0]).parent.parent.parent / 'run-tests.sh'), '-l']
cmd.extend(sys.argv[1:])
proc = subprocess.run(cmd, stdout = subprocess.PIPE, check = True, universal_newlines = True, encoding = 'utf-8')
count = 0
for line in proc.stdout.splitlines():
tests.select(line.strip())
count += 1
print(f"{count} test(s) found")
with GlusterTesting(tests) as test:
for name in iter(tests):
test.process(name)
if HOST_URL is not None:
with open('/tmp/monitor', 'r') as f:
requests.put(f'{HOST_URL}/run/monitor', data = f)