feat(gas-logger): добавь непрерывный логгер и JSON-карту
This commit is contained in:
22
c/gas-logger/tests/host_device.c
Normal file
22
c/gas-logger/tests/host_device.c
Normal file
@@ -0,0 +1,22 @@
|
||||
/* Host-only firmware harness for the actual PM35 port and Python client. */
|
||||
#include "gas_logger_pm35.h"
|
||||
#include "gas_logger_pcan.h"
|
||||
#include <assert.h>
|
||||
static gas_logger log;
|
||||
static uint16_t bank0[9120],bank1[9120],modbus[128],voltage[2];
|
||||
static uint32_t irq;
|
||||
static uint32_t enter(void *u) { uint32_t old=irq;(void)u;irq=1;return old; }
|
||||
static void leave(void *u,uint32_t state) { (void)u;assert(irq==1);irq=state; }
|
||||
static gl_pm35_context context={modbus,voltage,enter,leave,0};
|
||||
int host_init(void) { irq=0;return gl_pm35_init(&log,&context,bank0,bank1,9120); }
|
||||
int host_capture(uint32_t time,uint16_t value)
|
||||
{ modbus[28]=value;modbus[127]=128;voltage[0]=1234;voltage[1]=5678;return gl_capture(&log,time,1); }
|
||||
int host_read(uint16_t address,uint16_t *out,uint16_t count)
|
||||
{ return gl_modbus_read(&log,address,out,count); }
|
||||
int host_write(uint16_t address,uint16_t value) { return gl_modbus_write(&log,address,value); }
|
||||
int host_pcan_read(uint16_t address,uint16_t *out)
|
||||
{
|
||||
pcan_gas_region_t region;gl_pcan_region_init(®ion,&log);
|
||||
if(address<region.base || (uint32_t)address>=region.base+region.count) return GL_ADDRESS;
|
||||
return region.read(®ion,(uint16_t)(address-region.base),out);
|
||||
}
|
||||
46
c/gas-logger/tests/run_tests.py
Normal file
46
c/gas-logger/tests/run_tests.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Build in a temporary directory; run core + Python/C end-to-end tests."""
|
||||
import argparse
|
||||
import ctypes
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
parser=argparse.ArgumentParser()
|
||||
parser.add_argument('--cc',default=shutil.which('gcc'))
|
||||
args=parser.parse_args()
|
||||
if not args.cc:raise SystemExit('Specify --cc /path/to/gcc')
|
||||
root=Path(__file__).resolve().parents[1]
|
||||
templates=root.parents[1]
|
||||
sys.path.insert(0,str(templates/'python'))
|
||||
env=dict(os.environ)
|
||||
env['PATH']=str(Path(args.cc).resolve().parent)+os.pathsep+env.get('PATH','')
|
||||
flags=[args.cc,'-std=c99','-Wall','-Wextra','-Werror','-pedantic','-I'+str(root/'include')]
|
||||
with tempfile.TemporaryDirectory(prefix='gas-logger-') as directory:
|
||||
binary=Path(directory)/'core.exe'
|
||||
subprocess.run(flags+[str(root/'src/gas_logger.c'),str(root/'tests/test_gas_logger.c'),'-o',str(binary)],check=True,env=env)
|
||||
subprocess.run([str(binary)],check=True,env=env)
|
||||
lib=Path(directory)/('device.dll' if os.name=='nt' else 'device.so')
|
||||
subprocess.run(flags+([] if os.name=='nt' else ['-fPIC'])+['-shared','-I'+str(root/'ports/pm35'),
|
||||
'-I'+str(templates/'c/set-protocol/include'),
|
||||
str(root/'src/gas_logger.c'),str(root/'src/gas_logger_pcan.c'),
|
||||
str(root/'ports/pm35/gas_logger_pm35.c'),str(root/'tests/host_device.c'),
|
||||
'-o',str(lib)],check=True,env=env)
|
||||
# Child releases the loaded DLL before TemporaryDirectory cleanup on Windows.
|
||||
runner='''import ctypes,importlib.util,sys,unittest
|
||||
spec=importlib.util.spec_from_file_location('test_client',sys.argv[1])
|
||||
m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
|
||||
m.DEVICE=ctypes.CDLL(sys.argv[2])
|
||||
m.DEVICE.host_capture.argtypes=[ctypes.c_uint32,ctypes.c_uint16]
|
||||
m.DEVICE.host_read.argtypes=[ctypes.c_uint16,ctypes.POINTER(ctypes.c_uint16),ctypes.c_uint16]
|
||||
m.DEVICE.host_write.argtypes=[ctypes.c_uint16,ctypes.c_uint16]
|
||||
m.DEVICE.host_pcan_read.argtypes=[ctypes.c_uint16,ctypes.POINTER(ctypes.c_uint16)]
|
||||
r=unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromModule(m))
|
||||
sys.exit(not r.wasSuccessful())'''
|
||||
env['PYTHONPATH']=str(templates/'python')
|
||||
subprocess.run([sys.executable,'-c',runner,str(root/'tests/test_client.py'),str(lib)],check=True,env=env)
|
||||
subprocess.run([sys.executable,str(root/'tools/generate_map.py'),'--check'],check=True,env=env)
|
||||
111
c/gas-logger/tests/test_client.py
Normal file
111
c/gas-logger/tests/test_client.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Integration: Python client -> register callbacks -> compiled C PM35 logger."""
|
||||
import copy
|
||||
import ctypes
|
||||
import unittest
|
||||
from set_devices.gas_logger import GasLoggerClient, load_map, validate_map
|
||||
|
||||
DEVICE = None # set by run_tests.py before loading the suite
|
||||
|
||||
|
||||
class ClientTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.lib=DEVICE
|
||||
self.assertEqual(self.lib.host_init(),0)
|
||||
self.client=GasLoggerClient(self.read,self.write)
|
||||
self.client.start()
|
||||
for i in range(800):
|
||||
self.assertEqual(self.lib.host_capture(i,i),0)
|
||||
|
||||
def read(self,address,count):
|
||||
out=(ctypes.c_uint16*count)()
|
||||
result=self.lib.host_read(address,out,count)
|
||||
if result: raise RuntimeError('read '+str(result))
|
||||
return list(out)
|
||||
|
||||
def write(self,address,value):
|
||||
result=self.lib.host_write(address,value)
|
||||
if result: raise RuntimeError('write '+str(result))
|
||||
|
||||
def test_download_keeps_recording_and_order(self):
|
||||
def progress(done,total):
|
||||
for i in range(20): self.assertEqual(self.lib.host_capture(900+done+i,99),0)
|
||||
records=self.client.download(progress=progress)
|
||||
self.assertEqual([r['time_ms'] for r in records],list(range(40,800)))
|
||||
self.assertEqual([r['current'] for r in records],list(range(40,800)))
|
||||
self.assertTrue(all(r['zpt2']==5678 for r in records))
|
||||
self.assertEqual(self.client.status()[2],1)
|
||||
self.assertGreater(self.client.status()[6],0)
|
||||
|
||||
def test_cancel_retains_snapshot_and_explicit_resume(self):
|
||||
cancel=[False]
|
||||
def progress(done,total): cancel[0]=True
|
||||
with self.assertRaises(InterruptedError):
|
||||
self.client.download(cancelled=lambda:cancel[0],progress=progress)
|
||||
self.assertEqual(self.client.status()[2],3)
|
||||
with self.assertRaises(RuntimeError): self.client.download()
|
||||
self.assertEqual(len(self.client.download(resume=True)),760)
|
||||
|
||||
def test_partial_reply_leaves_pin(self):
|
||||
original=self.client.read
|
||||
self.client.read=lambda a,n: original(a,n)[:-1] if a>=0x6100 else original(a,n)
|
||||
with self.assertRaises(ValueError): self.client.download()
|
||||
self.client.read=original
|
||||
self.assertEqual(self.client.status()[2],3)
|
||||
|
||||
def test_no_writes_for_wrong_schema(self):
|
||||
m=load_map();m['channels'][0]['name']='Other'
|
||||
writes=[]
|
||||
client=GasLoggerClient(self.read,lambda *a:writes.append(a),m)
|
||||
with self.assertRaises(ValueError): client.download()
|
||||
self.assertEqual(writes,[])
|
||||
|
||||
def test_generation_mismatch_does_not_release(self):
|
||||
self.write(0x6012,2)
|
||||
with self.assertRaises(ValueError):self.client.release(999)
|
||||
self.assertEqual(self.client.status()[2],3)
|
||||
|
||||
def test_classic_can_chunks_and_pcan_region(self):
|
||||
def read(a,n):
|
||||
words=[]
|
||||
for i in range(n):
|
||||
out=ctypes.c_uint16()
|
||||
self.assertEqual(self.lib.host_pcan_read(a+i,ctypes.byref(out)),0)
|
||||
words.append(out.value)
|
||||
return words
|
||||
client=GasLoggerClient(read,self.write,block_words=4)
|
||||
self.assertEqual(len(client.download()),760)
|
||||
|
||||
def test_json_rejects_collisions_and_drift(self):
|
||||
base=load_map()
|
||||
for change in (
|
||||
lambda m:m['channels'][0].update(gas=0x6000),
|
||||
lambda m:m['channels'][1].update(gas=m['channels'][0]['gas']),
|
||||
lambda m:m['service'].update(data_offset=512),
|
||||
lambda m:m['service']['registers'][0].update(key='wrong'),
|
||||
lambda m:m.update(capacity=65535),
|
||||
):
|
||||
m=copy.deepcopy(base);change(m)
|
||||
with self.assertRaises(ValueError):validate_map(m)
|
||||
|
||||
def test_replaced_snapshot_is_rejected_before_release(self):
|
||||
changed=[False]
|
||||
def progress(done,total):
|
||||
if changed[0]:return
|
||||
changed[0]=True
|
||||
self.client.release(1)
|
||||
self.assertEqual(self.lib.host_capture(2000,999),0)
|
||||
self.write(0x6012,2)
|
||||
# Simulates a second, unsupported controlling client. We must never
|
||||
# accept a mixture of generations or release the newer snapshot.
|
||||
with self.assertRaises((ValueError,RuntimeError)):
|
||||
self.client.download(progress=progress)
|
||||
status=self.client.status()
|
||||
self.assertEqual(status[2],3)
|
||||
self.assertEqual(status[8],2)
|
||||
|
||||
def test_start_does_not_reset_history(self):
|
||||
before=self.client.status()
|
||||
self.client.start()
|
||||
after=self.client.status()
|
||||
self.assertEqual(before[6],after[6])
|
||||
self.assertEqual(before[12:14],after[12:14])
|
||||
54
c/gas-logger/tests/test_gas_logger.c
Normal file
54
c/gas-logger/tests/test_gas_logger.c
Normal file
@@ -0,0 +1,54 @@
|
||||
#include "gas_logger.h"
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
typedef struct { uint32_t irq; uint16_t value; int fail; } board;
|
||||
static uint32_t enter(void *u) { board *b=u; uint32_t old=b->irq; b->irq=1; return old; }
|
||||
static void leave(void *u,uint32_t state) { board *b=u; assert(b->irq==1); b->irq=state; }
|
||||
static int source(void *u,uint16_t a,uint16_t *v)
|
||||
{ board *b=u; assert(b->irq==1); if(b->fail && a==11) return 0; *v=b->value+a; return 1; }
|
||||
int main(void)
|
||||
{
|
||||
gl_channel channels[]={{10,100,200},{11,101,201}};
|
||||
gl_config c={0x6000,2,3,0x12345678UL,channels};
|
||||
board b={0,0,0};
|
||||
gl_port p={&b,enter,leave,source};
|
||||
gas_logger l;
|
||||
uint16_t bank0[21],bank1[21],out[125],i;
|
||||
assert(gl_init(&l,&c,&p,bank0,bank1,20)==GL_ARGUMENT);
|
||||
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_OK);
|
||||
assert(gl_snapshot(&l)==GL_EMPTY);
|
||||
assert(gl_capture(&l,0,0)==GL_BUSY);
|
||||
assert(gl_write(&l,0x6012,GL_START)==GL_OK);
|
||||
for(i=0;i<5;i++) { b.value=i; assert(gl_capture(&l,i,2)==GL_OK); }
|
||||
assert(gl_snapshot(&l)==GL_OK && l.recording && l.count==0);
|
||||
assert(gl_read(&l,0x6100,out,21)==GL_OK);
|
||||
assert(out[0]==2 && out[7]==3 && out[14]==4 && out[5]==12);
|
||||
for(i=5;i<15;i++) { b.value=i; assert(gl_capture(&l,i,3)==GL_OK); }
|
||||
assert(gl_snapshot(&l)==GL_BUSY && l.missed==1 && l.recording);
|
||||
assert(gl_read(&l,0x6100,out,21)==GL_OK && out[0]==2 && out[14]==4);
|
||||
assert(gl_read(&l,0x6114,out,2)==GL_ADDRESS);
|
||||
assert(gl_read(&l,65535,out,2)==GL_ARGUMENT);
|
||||
assert(gl_write(&l,0x6012,GL_RELEASE)==GL_STALE);
|
||||
assert(gl_write(&l,0x6013,1)==GL_OK);
|
||||
assert(gl_write(&l,0x6014,0)==GL_OK);
|
||||
assert(gl_write(&l,0x6012,GL_RELEASE)==GL_OK && l.recording);
|
||||
assert(gl_read(&l,0x6100,out,1)==GL_ADDRESS);
|
||||
assert(gl_snapshot(&l)==GL_OK);
|
||||
assert(gl_read(&l,0x6100,out,21)==GL_OK && out[0]==12 && out[14]==14);
|
||||
assert(gl_modbus_read(&l,100,out,2)==GL_OK && out[0]==24 && out[1]==25);
|
||||
assert(gl_can_read(&l,200,out,2)==GL_OK && out[0]==24 && out[1]==25);
|
||||
b.fail=1; out[0]=555;out[1]=666;
|
||||
assert(gl_read(&l,10,out,2)==GL_SOURCE_ERROR && out[0]==555 && out[1]==666);
|
||||
assert(gl_capture(&l,15,0)==GL_SOURCE_ERROR && l.count==0 && l.source_errors==1);
|
||||
b.fail=0;b.irq=1;
|
||||
assert(gl_capture(&l,16,0)==GL_OK && b.irq==1);
|
||||
b.irq=0;l.sequence=0xffffffffUL;
|
||||
assert(gl_capture(&l,0xffffffffUL,0)==GL_OK && l.sequence==0);
|
||||
assert(gl_write(&l,0x6012,0)==GL_ARGUMENT && l.recording);
|
||||
c.base=0xff00;
|
||||
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_ARGUMENT);
|
||||
c.base=0x6000;channels[1].can=200;
|
||||
assert(gl_init(&l,&c,&p,bank0,bank1,21)==GL_ARGUMENT);
|
||||
assert(b.irq==0);
|
||||
puts("GAS logger core: PASS");return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user