diff --git a/tests/restful_client/api/alias.py b/tests/restful_client/api/alias.py new file mode 100644 index 0000000000000000000000000000000000000000..b67a08d0a7f054ece68dfd7fa35ee5ab2a559676 --- /dev/null +++ b/tests/restful_client/api/alias.py @@ -0,0 +1,17 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Alias(RestClient): + + def drop_alias(): + pass + + def alter_alias(): + pass + + def create_alias(): + pass + \ No newline at end of file diff --git a/tests/restful_client/api/collection.py b/tests/restful_client/api/collection.py new file mode 100644 index 0000000000000000000000000000000000000000..a5f542dfac64f65fbbf2c9ce9c9b319ab1863913 --- /dev/null +++ b/tests/restful_client/api/collection.py @@ -0,0 +1,62 @@ +import json + +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Collection(RestClient): + + @DELETE("collection") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def drop_collection(self, payload): + """Drop a collection""" + + @GET("collection") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def describe_collection(self, payload): + """Describe a collection""" + + @POST("collection") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def create_collection(self, payload): + """Create a collection""" + + @GET("collection/existence") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def has_collection(self, payload): + """Check if a collection exists""" + + @DELETE("collection/load") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def release_collection(self, payload): + """Release a collection""" + + @POST("collection/load") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def load_collection(self, payload): + """Load a collection""" + + @GET("collection/statistics") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_collection_statistics(self, payload): + """Get collection statistics""" + + @GET("collections") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def show_collections(self, payload): + """Show collections""" + + +if __name__ == '__main__': + client = Collection("http://localhost:19121/api/v1") + print(client) diff --git a/tests/restful_client/api/credential.py b/tests/restful_client/api/credential.py new file mode 100644 index 0000000000000000000000000000000000000000..eb4cc3b535e23e2d1cca2ed18775484794ecd7a4 --- /dev/null +++ b/tests/restful_client/api/credential.py @@ -0,0 +1,19 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Credential(RestClient): + + def delete_credential(): + pass + + def update_credential(): + pass + + def create_credential(): + pass + + def list_credentials(): + pass diff --git a/tests/restful_client/api/entity.py b/tests/restful_client/api/entity.py new file mode 100644 index 0000000000000000000000000000000000000000..fc7a98dd93836cfebbe7cc3575f128b6f7241f41 --- /dev/null +++ b/tests/restful_client/api/entity.py @@ -0,0 +1,63 @@ +import json +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Entity(RestClient): + + @POST("distance") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def calc_distance(self, payload): + """ Calculate distance between two points """ + + @DELETE("entities") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def delete(self, payload): + """delete entities""" + + @POST("entities") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def insert(self, payload): + """insert entities""" + + @POST("persist") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def flush(self, payload): + """flush entities""" + + @POST("persist/segment-info") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_persistent_segment_info(self, payload): + """get persistent segment info""" + + @POST("persist/state") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_flush_state(self, payload): + """get flush state""" + + @POST("query") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def query(self, payload): + """query entities""" + + @POST("query-segment-info") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_query_segment_info(self, payload): + """get query segment info""" + + @POST("search") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def search(self, payload): + """search entities""" + \ No newline at end of file diff --git a/tests/restful_client/api/import.py b/tests/restful_client/api/import.py new file mode 100644 index 0000000000000000000000000000000000000000..4b0ed2f2cbea1396b33ed394055080f5bb7a066b --- /dev/null +++ b/tests/restful_client/api/import.py @@ -0,0 +1,18 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + +class Import(RestClient): + + def list_import_tasks(): + pass + + def exec_import(): + pass + + def get_import_state(): + pass + + + diff --git a/tests/restful_client/api/index.py b/tests/restful_client/api/index.py new file mode 100644 index 0000000000000000000000000000000000000000..c0c4f6a341e1d1415a6510bb8ec8e8b78c574999 --- /dev/null +++ b/tests/restful_client/api/index.py @@ -0,0 +1,38 @@ +import json +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Index(RestClient): + + @DELETE("/index") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def drop_index(self, payload): + """Drop an index""" + + @GET("/index") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def describe_index(self, payload): + """Describe an index""" + + @POST("index") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def create_index(self, payload): + """create index""" + + @GET("index/progress") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_index_build_progress(self, payload): + """get index build progress""" + + @GET("index/state") + @body("payload", lambda p: json.dumps(p)) + @on(200, lambda r: r.json()) + def get_index_state(self, payload): + """get index state""" diff --git a/tests/restful_client/api/metrics.py b/tests/restful_client/api/metrics.py new file mode 100644 index 0000000000000000000000000000000000000000..c02a9c7f4fc3819948f4e08907c09fd2b346474e --- /dev/null +++ b/tests/restful_client/api/metrics.py @@ -0,0 +1,11 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Metrics(RestClient): + + def get_metrics(): + pass + \ No newline at end of file diff --git a/tests/restful_client/api/ops.py b/tests/restful_client/api/ops.py new file mode 100644 index 0000000000000000000000000000000000000000..4859c334f0ac80f520afe6d2e4bd0ea2c980bb47 --- /dev/null +++ b/tests/restful_client/api/ops.py @@ -0,0 +1,21 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + +class Ops(RestClient): + + def manual_compaction(): + pass + + def get_compaction_plans(): + pass + + def get_compaction_state(): + pass + + def load_balance(): + pass + + def get_replicas(): + pass \ No newline at end of file diff --git a/tests/restful_client/api/partition.py b/tests/restful_client/api/partition.py new file mode 100644 index 0000000000000000000000000000000000000000..845b646617840cb633318bbd4589778fb0d2bdd5 --- /dev/null +++ b/tests/restful_client/api/partition.py @@ -0,0 +1,27 @@ +from decorest import GET, POST, DELETE +from decorest import HttpStatus, RestClient +from decorest import accept, body, content, endpoint, form +from decorest import header, multipart, on, query, stream, timeout + + +class Partition(RestClient): + def drop_partition(): + pass + + def create_partition(): + pass + + def has_partition(): + pass + + def get_partition_statistics(): + pass + + def show_partitions(): + pass + + def release_partition(): + pass + + def load_partition(): + pass diff --git a/tests/restful_client/api/pydantic_demo.py b/tests/restful_client/api/pydantic_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..43d901f2225284d52ead20f557596c1d837d94be --- /dev/null +++ b/tests/restful_client/api/pydantic_demo.py @@ -0,0 +1,43 @@ +from datetime import date, datetime +from typing import List, Union, Optional + +from pydantic import BaseModel, UUID4, conlist + +from pydantic_factories import ModelFactory + + +class Person(BaseModel): + def __init__(self, length): + super().__init__() + self.len = length + id: UUID4 + name: str + hobbies: List[str] + age: Union[float, int] + birthday: Union[datetime, date] + + +class Pet(BaseModel): + name: str + age: int + + +class PetFactory(BaseModel): + name: str + pet: Pet + age: Optional[int] = None + + +sample = { + "name": "John", + "pet": { + "name": "Fido", + "age": 3 + } + +} + +result = PetFactory(**sample) + +print(result) + diff --git a/tests/restful_client/base/alias_service.py b/tests/restful_client/base/alias_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/base/client_base.py b/tests/restful_client/base/client_base.py new file mode 100644 index 0000000000000000000000000000000000000000..2e52f88bc5d9a07b67563f79b279a39686269149 --- /dev/null +++ b/tests/restful_client/base/client_base.py @@ -0,0 +1,72 @@ +from time import sleep + +from decorest import HttpStatus, RestClient +from models.schema import CollectionSchema +from base.collection_service import CollectionService +from base.index_service import IndexService +from base.entity_service import EntityService + +from utils.util_log import test_log as log +from common import common_func as cf +from common import common_type as ct + +class Base: + """init base class""" + + endpoint = None + collection_service = None + index_service = None + entity_service = None + collection_name = None + collection_object_list = [] + + def setup_class(self): + log.info("setup class") + + def teardown_class(self): + log.info("teardown class") + + def setup_method(self, method): + log.info(("*" * 35) + " setup " + ("*" * 35)) + log.info("[setup_method] Start setup test case %s." % method.__name__) + host = cf.param_info.param_host + port = cf.param_info.param_port + self.endpoint = "http://" + host + ":" + str(port) + "/api/v1" + self.collection_service = CollectionService(self.endpoint) + self.index_service = IndexService(self.endpoint) + self.entity_service = EntityService(self.endpoint) + + def teardown_method(self, method): + res = self.collection_service.has_collection(collection_name=self.collection_name) + log.info(f"collection {self.collection_name} exists: {res}") + if res["value"] is True: + res = self.collection_service.drop_collection(self.collection_name) + log.info(f"drop collection {self.collection_name} res: {res}") + res = self.collection_service.show_collections() + all_collections = res["collection_names"] + union_collections = set(all_collections) & set(self.collection_object_list) + for collection in union_collections: + res = self.collection_service.drop_collection(collection) + log.info(f"drop collection {collection} res: {res}") + log.info("[teardown_method] Start teardown test case %s." % method.__name__) + log.info(("*" * 35) + " teardown " + ("*" * 35)) + + +class TestBase(Base): + """init test base class""" + + def init_collection(self, name=None, schema=None): + collection_name = cf.gen_unique_str("test") if name is None else name + self.collection_name = collection_name + self.collection_object_list.append(collection_name) + if schema is None: + schema = cf.gen_default_schema(collection_name=collection_name) + # create collection + res = self.collection_service.create_collection(collection_name=collection_name, schema=schema) + + log.info(f"create collection name: {collection_name} with schema: {schema}") + return collection_name, schema + + + + diff --git a/tests/restful_client/base/collection_service.py b/tests/restful_client/base/collection_service.py new file mode 100644 index 0000000000000000000000000000000000000000..292464f451448e780998851b691e8143bcde259a --- /dev/null +++ b/tests/restful_client/base/collection_service.py @@ -0,0 +1,96 @@ +from api.collection import Collection +from utils.util_log import test_log as log +from models import milvus + + +TIMEOUT = 30 + + +class CollectionService: + + def __init__(self, endpoint=None, timeout=None): + if timeout is None: + timeout = TIMEOUT + if endpoint is None: + endpoint = "http://localhost:9091/api/v1" + self._collection = Collection(endpoint=endpoint) + + def create_collection(self, collection_name, consistency_level=1, schema=None, shards_num=2): + payload = { + "collection_name": collection_name, + "consistency_level": consistency_level, + "schema": schema, + "shards_num": shards_num + } + log.info(f"payload: {payload}") + # payload = milvus.CreateCollectionRequest(collection_name=collection_name, + # consistency_level=consistency_level, + # schema=schema, + # shards_num=shards_num) + # payload = payload.dict() + rsp = self._collection.create_collection(payload) + return rsp + + def has_collection(self, collection_name=None, time_stamp=0): + payload = { + "collection_name": collection_name, + "time_stamp": time_stamp + } + # payload = milvus.HasCollectionRequest(collection_name=collection_name, time_stamp=time_stamp) + # payload = payload.dict() + return self._collection.has_collection(payload) + + def drop_collection(self, collection_name): + payload = { + "collection_name": collection_name + } + # payload = milvus.DropCollectionRequest(collection_name=collection_name) + # payload = payload.dict() + return self._collection.drop_collection(payload) + + def describe_collection(self, collection_name, collection_id=None, time_stamp=0): + payload = { + "collection_name": collection_name, + "collection_id": collection_id, + "time_stamp": time_stamp + } + # payload = milvus.DescribeCollectionRequest(collection_name=collection_name, + # collectionID=collection_id, + # time_stamp=time_stamp) + # payload = payload.dict() + return self._collection.describe_collection(payload) + + def load_collection(self, collection_name, replica_number=1): + payload = { + "collection_name": collection_name, + "replica_number": replica_number + } + # payload = milvus.LoadCollectionRequest(collection_name=collection_name, replica_number=replica_number) + # payload = payload.dict() + return self._collection.load_collection(payload) + + def release_collection(self, collection_name): + payload = { + "collection_name": collection_name + } + # payload = milvus.ReleaseCollectionRequest(collection_name=collection_name) + # payload = payload.dict() + return self._collection.release_collection(payload) + + def get_collection_statistics(self, collection_name): + payload = { + "collection_name": collection_name + } + # payload = milvus.GetCollectionStatisticsRequest(collection_name=collection_name) + # payload = payload.dict() + return self._collection.get_collection_statistics(payload) + + def show_collections(self, collection_names=None, type=0): + + payload = { + "collection_names": collection_names, + "type": type + } + # payload = milvus.ShowCollectionsRequest(collection_names=collection_names, type=type) + # payload = payload.dict() + return self._collection.show_collections(payload) diff --git a/tests/restful_client/base/credential_service.py b/tests/restful_client/base/credential_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/base/entity_service.py b/tests/restful_client/base/entity_service.py new file mode 100644 index 0000000000000000000000000000000000000000..cf5464ecc6230176f3f8565c0fb91731c807ab96 --- /dev/null +++ b/tests/restful_client/base/entity_service.py @@ -0,0 +1,182 @@ +from api.entity import Entity +from common import common_type as ct +from utils.util_log import test_log as log +from models import common, schema, milvus, server + +TIMEOUT = 30 + + +class EntityService: + + def __init__(self, endpoint=None, timeout=None): + if timeout is None: + timeout = TIMEOUT + if endpoint is None: + endpoint = "http://localhost:9091/api/v1" + self._entity = Entity(endpoint=endpoint) + + def calc_distance(self, base=None, op_left=None, op_right=None, params=None): + payload = { + "base": base, + "op_left": op_left, + "op_right": op_right, + "params": params + } + # payload = milvus.CalcDistanceRequest(base=base, op_left=op_left, op_right=op_right, params=params) + # payload = payload.dict() + return self._entity.calc_distance(payload) + + def delete(self, base=None, collection_name=None, db_name=None, expr=None, hash_keys=None, partition_name=None): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name, + "expr": expr, + "hash_keys": hash_keys, + "partition_name": partition_name + } + # payload = server.DeleteRequest(base=base, + # collection_name=collection_name, + # db_name=db_name, + # expr=expr, + # hash_keys=hash_keys, + # partition_name=partition_name) + # payload = payload.dict() + return self._entity.delete(payload) + + def insert(self, base=None, collection_name=None, db_name=None, fields_data=None, hash_keys=None, num_rows=None, + partition_name=None, check_task=True): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name, + "fields_data": fields_data, + "hash_keys": hash_keys, + "num_rows": num_rows, + "partition_name": partition_name + } + # payload = milvus.InsertRequest(base=base, + # collection_name=collection_name, + # db_name=db_name, + # fields_data=fields_data, + # hash_keys=hash_keys, + # num_rows=num_rows, + # partition_name=partition_name) + # payload = payload.dict() + rsp = self._entity.insert(payload) + if check_task: + assert rsp["status"] == {} + assert rsp["insert_cnt"] == num_rows + return rsp + + def flush(self, base=None, collection_names=None, db_name=None, check_task=True): + payload = { + "base": base, + "collection_names": collection_names, + "db_name": db_name + } + # payload = server.FlushRequest(base=base, + # collection_names=collection_names, + # db_name=db_name) + # payload = payload.dict() + rsp = self._entity.flush(payload) + if check_task: + assert rsp["status"] == {} + + def get_persistent_segment_info(self, base=None, collection_name=None, db_name=None): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name + } + # payload = server.GetPersistentSegmentInfoRequest(base=base, + # collection_name=collection_name, + # db_name=db_name) + # payload = payload.dict() + return self._entity.get_persistent_segment_info(payload) + + def get_flush_state(self, segment_ids=None): + payload = { + "segment_ids": segment_ids + } + # payload = server.GetFlushStateRequest(segment_ids=segment_ids) + # payload = payload.dict() + return self._entity.get_flush_state(payload) + + def query(self, base=None, collection_name=None, db_name=None, expr=None, + guarantee_timestamp=None, output_fields=None, partition_names=None, travel_timestamp=None, + check_task=True): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name, + "expr": expr, + "guarantee_timestamp": guarantee_timestamp, + "output_fields": output_fields, + "partition_names": partition_names, + "travel_timestamp": travel_timestamp + } + # + # payload = server.QueryRequest(base=base, collection_name=collection_name, db_name=db_name, expr=expr, + # guarantee_timestamp=guarantee_timestamp, output_fields=output_fields, + # partition_names=partition_names, travel_timestamp=travel_timestamp) + # payload = payload.dict() + rsp = self._entity.query(payload) + if check_task: + fields_data = rsp["fields_data"] + for field_data in fields_data: + if field_data["field_name"] in expr: + data = field_data["Field"]["Scalars"]["Data"]["LongData"]["data"] + for d in data: + s = expr.replace(field_data["field_name"], str(d)) + assert eval(s) is True + return rsp + + def get_query_segment_info(self, base=None, collection_name=None, db_name=None): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name + } + # payload = server.GetQuerySegmentInfoRequest(base=base, + # collection_name=collection_name, + # db_name=db_name) + # payload = payload.dict() + return self._entity.get_query_segment_info(payload) + + def search(self, base=None, collection_name=None, vectors=None, db_name=None, dsl=None, + output_fields=None, dsl_type=1, + guarantee_timestamp=None, partition_names=None, placeholder_group=None, + search_params=None, travel_timestamp=None, check_task=True): + payload = { + "base": base, + "collection_name": collection_name, + "output_fields": output_fields, + "vectors": vectors, + "db_name": db_name, + "dsl": dsl, + "dsl_type": dsl_type, + "guarantee_timestamp": guarantee_timestamp, + "partition_names": partition_names, + "placeholder_group": placeholder_group, + "search_params": search_params, + "travel_timestamp": travel_timestamp + } + # payload = server.SearchRequest(base=base, collection_name=collection_name, db_name=db_name, dsl=dsl, + # dsl_type=dsl_type, guarantee_timestamp=guarantee_timestamp, + # partition_names=partition_names, placeholder_group=placeholder_group, + # search_params=search_params, travel_timestamp=travel_timestamp) + # payload = payload.dict() + rsp = self._entity.search(payload) + if check_task: + assert rsp["status"] == {} + assert rsp["results"]["num_queries"] == len(vectors) + assert len(rsp["results"]["ids"]["IdField"]["IntId"]["data"]) == sum(rsp["results"]["topks"]) + return rsp + + + + + + + diff --git a/tests/restful_client/base/import_service.py b/tests/restful_client/base/import_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/base/index_service.py b/tests/restful_client/base/index_service.py new file mode 100644 index 0000000000000000000000000000000000000000..ac48f35541b069fb4ae16367a4328ee20b75fe92 --- /dev/null +++ b/tests/restful_client/base/index_service.py @@ -0,0 +1,57 @@ +from api.index import Index +from models import common, schema, milvus, server + +TIMEOUT = 30 + + +class IndexService: + + def __init__(self, endpoint=None, timeout=None): + if timeout is None: + timeout = TIMEOUT + if endpoint is None: + endpoint = "http://localhost:9091/api/v1" + self._index = Index(endpoint=endpoint) + + def drop_index(self, base, collection_name, db_name, field_name, index_name): + payload = server.DropIndexRequest(base=base, collection_name=collection_name, + db_name=db_name, field_name=field_name, index_name=index_name) + payload = payload.dict() + return self._index.drop_index(payload) + + def describe_index(self, base, collection_name, db_name, field_name, index_name): + payload = server.DescribeIndexRequest(base=base, collection_name=collection_name, + db_name=db_name, field_name=field_name, index_name=index_name) + payload = payload.dict() + return self._index.describe_index(payload) + + def create_index(self, base=None, collection_name=None, db_name=None, extra_params=None, + field_name=None, index_name=None): + payload = { + "base": base, + "collection_name": collection_name, + "db_name": db_name, + "extra_params": extra_params, + "field_name": field_name, + "index_name": index_name + } + # payload = server.CreateIndexRequest(base=base, collection_name=collection_name, db_name=db_name, + # extra_params=extra_params, field_name=field_name, index_name=index_name) + # payload = payload.dict() + return self._index.create_index(payload) + + def get_index_build_progress(self, base, collection_name, db_name, field_name, index_name): + payload = server.GetIndexBuildProgressRequest(base=base, collection_name=collection_name, + db_name=db_name, field_name=field_name, index_name=index_name) + payload = payload.dict() + return self._index.get_index_build_progress(payload) + + def get_index_state(self, base, collection_name, db_name, field_name, index_name): + payload = server.GetIndexStateRequest(base=base, collection_name=collection_name, + db_name=db_name, field_name=field_name, index_name=index_name) + payload = payload.dict() + return self._index.get_index_state(payload) + + + + diff --git a/tests/restful_client/base/metrics_service.py b/tests/restful_client/base/metrics_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/base/ops_service.py b/tests/restful_client/base/ops_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/base/partition_service.py b/tests/restful_client/base/partition_service.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/restful_client/check/func_check.py b/tests/restful_client/check/func_check.py new file mode 100644 index 0000000000000000000000000000000000000000..13a1711e07baa517dbd7e04cf361738ba437be90 --- /dev/null +++ b/tests/restful_client/check/func_check.py @@ -0,0 +1,27 @@ + +class CheckTasks: + """ The name of the method used to check the result """ + check_nothing = "check_nothing" + err_res = "error_response" + ccr = "check_connection_result" + check_collection_property = "check_collection_property" + check_partition_property = "check_partition_property" + check_search_results = "check_search_results" + check_query_results = "check_query_results" + check_query_empty = "check_query_empty" # verify that query result is empty + check_query_not_empty = "check_query_not_empty" + check_distance = "check_distance" + check_delete_compact = "check_delete_compact" + check_merge_compact = "check_merge_compact" + check_role_property = "check_role_property" + check_permission_deny = "check_permission_deny" + check_value_equal = "check_value_equal" + + +class ResponseChecker: + + def __init__(self, check_task, check_items): + self.check_task = check_task + self.check_items = check_items + + diff --git a/tests/restful_client/check/param_check.py b/tests/restful_client/check/param_check.py new file mode 100644 index 0000000000000000000000000000000000000000..d4938f950c849bdad35fc3fb78506b6de6175eb9 --- /dev/null +++ b/tests/restful_client/check/param_check.py @@ -0,0 +1,21 @@ +from utils.util_log import test_log as log + + +def ip_check(ip): + if ip == "localhost": + return True + + if not isinstance(ip, str): + log.error("[IP_CHECK] IP(%s) is not a string." % ip) + return False + + return True + + +def number_check(num): + if str(num).isdigit(): + return True + + else: + log.error("[NUMBER_CHECK] Number(%s) is not a numbers." % num) + return False diff --git a/tests/restful_client/common/common_func.py b/tests/restful_client/common/common_func.py new file mode 100644 index 0000000000000000000000000000000000000000..5e95fa174ba3f0dcb37640091fb7f63d32067a6c --- /dev/null +++ b/tests/restful_client/common/common_func.py @@ -0,0 +1,292 @@ +import json +import os +import random +import string +import numpy as np +from enum import Enum +from common import common_type as ct +from utils.util_log import test_log as log + + +class ParamInfo: + def __init__(self): + self.param_host = "" + self.param_port = "" + + def prepare_param_info(self, host, http_port): + self.param_host = host + self.param_port = http_port + + +param_info = ParamInfo() + + +class DataType(Enum): + Bool: 1 + Int8: 2 + Int16: 3 + Int32: 4 + Int64: 5 + Float: 10 + Double: 11 + String: 20 + VarChar: 21 + BinaryVector: 100 + FloatVector: 101 + + +def gen_unique_str(str_value=None): + prefix = "".join(random.choice(string.ascii_letters + string.digits) for _ in range(8)) + return "test_" + prefix if str_value is None else str_value + "_" + prefix + + +def gen_field(name=ct.default_bool_field_name, description=ct.default_desc, type_params=None, index_params=None, + data_type="Int64", is_primary_key=False, auto_id=False, dim=128, max_length=256): + data_type_map = { + "Bool": 1, + "Int8": 2, + "Int16": 3, + "Int32": 4, + "Int64": 5, + "Float": 10, + "Double": 11, + "String": 20, + "VarChar": 21, + "BinaryVector": 100, + "FloatVector": 101, + } + if data_type == "Int64": + is_primary_key = True + auto_id = True + if type_params is None: + type_params = [] + if index_params is None: + index_params = [] + if data_type in ["FloatVector", "BinaryVector"]: + type_params = [{"key": "dim", "value": str(dim)}] + if data_type in ["String", "VarChar"]: + type_params = [{"key": "max_length", "value": str(dim)}] + return { + "name": name, + "description": description, + "data_type": data_type_map.get(data_type, 0), + "type_params": type_params, + "index_params": index_params, + "is_primary_key": is_primary_key, + "auto_id": auto_id, + } + + +def gen_schema(name, fields, description=ct.default_desc, auto_id=False): + return { + "name": name, + "description": description, + "auto_id": auto_id, + "fields": fields, + } + + +def gen_default_schema(data_types=None, dim=ct.default_dim, collection_name=None): + if data_types is None: + data_types = ["Int64", "Float", "VarChar", "FloatVector"] + fields = [] + for data_type in data_types: + if data_type in ["FloatVector", "BinaryVector"]: + fields.append(gen_field(name=data_type, data_type=data_type, type_params=[{"key": "dim", "value": dim}])) + else: + fields.append(gen_field(name=data_type, data_type=data_type)) + return { + "autoID": True, + "fields": fields, + "description": ct.default_desc, + "name": collection_name, + } + + +def gen_fields_data(schema=None, nb=ct.default_nb,): + if schema is None: + schema = gen_default_schema() + fields = schema["fields"] + fields_data = [] + for field in fields: + if field["data_type"] == 1: + fields_data.append([random.choice([True, False]) for i in range(nb)]) + elif field["data_type"] == 2: + fields_data.append([i for i in range(nb)]) + elif field["data_type"] == 3: + fields_data.append([i for i in range(nb)]) + elif field["data_type"] == 4: + fields_data.append([i for i in range(nb)]) + elif field["data_type"] == 5: + fields_data.append([i for i in range(nb)]) + elif field["data_type"] == 10: + fields_data.append([np.float64(i) for i in range(nb)]) # json not support float32 + elif field["data_type"] == 11: + fields_data.append([np.float64(i) for i in range(nb)]) + elif field["data_type"] == 20: + fields_data.append([gen_unique_str((str(i))) for i in range(nb)]) + elif field["data_type"] == 21: + fields_data.append([gen_unique_str(str(i)) for i in range(nb)]) + elif field["data_type"] == 100: + dim = ct.default_dim + for k, v in field["type_params"]: + if k == "dim": + dim = int(v) + break + fields_data.append(gen_binary_vectors(nb, dim)) + elif field["data_type"] == 101: + dim = ct.default_dim + for k, v in field["type_params"]: + if k == "dim": + dim = int(v) + break + fields_data.append(gen_float_vectors(nb, dim)) + else: + log.error("Unknown data type.") + fields_data_body = [] + for i, field in enumerate(fields): + fields_data_body.append({ + "field_name": field["name"], + "type": field["data_type"], + "field": fields_data[i], + }) + return fields_data_body + + +def get_vector_field(schema): + for field in schema["fields"]: + if field["data_type"] in [100, 101]: + return field["name"] + return None + + +def get_varchar_field(schema): + for field in schema["fields"]: + if field["data_type"] == 21: + return field["name"] + return None + + +def gen_vectors(nq=None, schema=None): + if nq is None: + nq = ct.default_nq + dim = ct.default_dim + data_type = 101 + for field in schema["fields"]: + if field["data_type"] in [100, 101]: + dim = ct.default_dim + data_type = field["data_type"] + for k, v in field["type_params"]: + if k == "dim": + dim = int(v) + break + if data_type == 100: + return gen_binary_vectors(nq, dim) + if data_type == 101: + return gen_float_vectors(nq, dim) + + +def gen_float_vectors(nb, dim): + return [[np.float64(random.uniform(-1.0, 1.0)) for _ in range(dim)] for _ in range(nb)] # json not support float32 + + +def gen_binary_vectors(nb, dim): + raw_vectors = [] + binary_vectors = [] + for _ in range(nb): + raw_vector = [random.randint(0, 1) for _ in range(dim)] + raw_vectors.append(raw_vector) + # packs a binary-valued array into bits in a unit8 array, and bytes array_of_ints + binary_vectors.append(bytes(np.packbits(raw_vector, axis=-1).tolist())) + return binary_vectors + + +def gen_index_params(index_type=None): + if index_type is None: + index_params = ct.default_index_params + else: + index_params = ct.all_index_params_map[index_type] + extra_params = [] + for k, v in index_params.items(): + item = {"key": k, "value": json.dumps(v) if isinstance(v, dict) else str(v)} + extra_params.append(item) + return extra_params + +def gen_search_param_by_index_type(index_type, metric_type="L2"): + search_params = [] + if index_type in ["FLAT", "IVF_FLAT", "IVF_SQ8", "IVF_PQ"]: + for nprobe in [10]: + ivf_search_params = {"metric_type": metric_type, "params": {"nprobe": nprobe}} + search_params.append(ivf_search_params) + elif index_type in ["BIN_FLAT", "BIN_IVF_FLAT"]: + for nprobe in [10]: + bin_search_params = {"metric_type": "HAMMING", "params": {"nprobe": nprobe}} + search_params.append(bin_search_params) + elif index_type in ["HNSW"]: + for ef in [64]: + hnsw_search_param = {"metric_type": metric_type, "params": {"ef": ef}} + search_params.append(hnsw_search_param) + elif index_type == "ANNOY": + for search_k in [1000]: + annoy_search_param = {"metric_type": metric_type, "params": {"search_k": search_k}} + search_params.append(annoy_search_param) + else: + log.info("Invalid index_type.") + raise Exception("Invalid index_type.") + return search_params + + +def gen_search_params(index_type=None, anns_field=ct.default_float_vec_field_name, + topk=ct.default_top_k): + if index_type is None: + search_params = gen_search_param_by_index_type(ct.default_index_type)[0] + else: + search_params = gen_search_param_by_index_type(index_type)[0] + extra_params = [] + for k, v in search_params.items(): + item = {"key": k, "value": json.dumps(v) if isinstance(v, dict) else str(v)} + extra_params.append(item) + extra_params.append({"key": "anns_field", "value": anns_field}) + extra_params.append({"key": "topk", "value": str(topk)}) + return extra_params + + + + +def gen_search_vectors(dim, nb, is_binary=False): + if is_binary: + return gen_binary_vectors(nb, dim) + return gen_float_vectors(nb, dim) + + +def modify_file(file_path_list, is_modify=False, input_content=""): + """ + file_path_list : file list -> list[] + is_modify : does the file need to be reset + input_content :the content that need to insert to the file + """ + if not isinstance(file_path_list, list): + log.error("[modify_file] file is not a list.") + + for file_path in file_path_list: + folder_path, file_name = os.path.split(file_path) + if not os.path.isdir(folder_path): + log.debug("[modify_file] folder(%s) is not exist." % folder_path) + os.makedirs(folder_path) + + if not os.path.isfile(file_path): + log.error("[modify_file] file(%s) is not exist." % file_path) + else: + if is_modify is True: + log.debug("[modify_file] start modifying file(%s)..." % file_path) + with open(file_path, "r+") as f: + f.seek(0) + f.truncate() + f.write(input_content) + f.close() + log.info("[modify_file] file(%s) modification is complete." % file_path_list) + + +if __name__ == '__main__': + a = gen_binary_vectors(10, 128) + print(a) diff --git a/tests/restful_client/common/common_type.py b/tests/restful_client/common/common_type.py new file mode 100644 index 0000000000000000000000000000000000000000..498f8bfc61a14d24991cac55a7d76c36dea9f08b --- /dev/null +++ b/tests/restful_client/common/common_type.py @@ -0,0 +1,80 @@ +""" Initialized parameters """ +port = 19530 +epsilon = 0.000001 +namespace = "milvus" +default_flush_interval = 1 +big_flush_interval = 1000 +default_drop_interval = 3 +default_dim = 128 +default_nb = 3000 +default_nb_medium = 5000 +default_top_k = 10 +default_nq = 2 +default_limit = 10 +default_search_params = {"metric_type": "L2", "params": {"nprobe": 10}} +default_search_ip_params = {"metric_type": "IP", "params": {"nprobe": 10}} +default_search_binary_params = {"metric_type": "JACCARD", "params": {"nprobe": 10}} +default_index_type = "HNSW" +default_index_params = {"index_type": "HNSW", "params": {"M": 48, "efConstruction": 500}, "metric_type": "L2"} +default_varchar_index = {} +default_binary_index = {"index_type": "BIN_IVF_FLAT", "params": {"nlist": 128}, "metric_type": "JACCARD"} +default_diskann_index = {"index_type": "DISKANN", "metric_type": "L2", "params": {}} +default_diskann_search_params = {"metric_type": "L2", "params": {"search_list": 30}} +max_top_k = 16384 +max_partition_num = 4096 # 256 +default_segment_row_limit = 1000 +default_server_segment_row_limit = 1024 * 512 +default_alias = "default" +default_user = "root" +default_password = "Milvus" +default_bool_field_name = "Bool" +default_int8_field_name = "Int8" +default_int16_field_name = "Int16" +default_int32_field_name = "Int32" +default_int64_field_name = "Int64" +default_float_field_name = "Float" +default_double_field_name = "Double" +default_string_field_name = "Varchar" +default_float_vec_field_name = "FloatVector" +another_float_vec_field_name = "FloatVector1" +default_binary_vec_field_name = "BinaryVector" +default_partition_name = "_default" +default_tag = "1970_01_01" +row_count = "row_count" +default_length = 65535 +default_desc = "" +default_collection_desc = "default collection" +default_index_name = "default_index_name" +default_binary_desc = "default binary collection" +collection_desc = "collection" +int_field_desc = "int64 type field" +float_field_desc = "float type field" +float_vec_field_desc = "float vector type field" +binary_vec_field_desc = "binary vector type field" +max_dim = 32768 +min_dim = 1 +gracefulTime = 1 +default_nlist = 128 +compact_segment_num_threshold = 4 +compact_delta_ratio_reciprocal = 5 # compact_delta_binlog_ratio is 0.2 +compact_retention_duration = 40 # compaction travel time retention range 20s +max_compaction_interval = 60 # the max time interval (s) from the last compaction +max_field_num = 256 # Maximum number of fields in a collection + +default_dsl = f"{default_int64_field_name} in [2,4,6,8]" +default_expr = f"{default_int64_field_name} in [2,4,6,8]" + +metric_types = [] +all_index_types = ["FLAT", "IVF_FLAT", "IVF_SQ8", "IVF_PQ", "HNSW", "ANNOY", "DISKANN", "BIN_FLAT", "BIN_IVF_FLAT"] +all_index_params_map = {"FLAT": {"index_type": "FLAT", "params": {"nlist": 128}, "metric_type": "L2"}, + "IVF_FLAT": {"index_type": "IVF_FLAT", "params": {"nlist": 128}, "metric_type": "L2"}, + "IVF_SQ8": {"index_type": "IVF_SQ8", "params": {"nlist": 128}, "metric_type": "L2"}, + "IVF_PQ": {"index_type": "IVF_PQ", "params": {"nlist": 128, "m": 16, "nbits": 8}, + "metric_type": "L2"}, + "HNSW": {"index_type": "HNSW", "params": {"M": 48, "efConstruction": 500}, "metric_type": "L2"}, + "ANNOY": {"index_type": "ANNOY", "params": {"n_trees": 50}, "metric_type": "L2"}, + "DISKANN": {"index_type": "DISKANN", "params": {}, "metric_type": "L2"}, + "BIN_FLAT": {"index_type": "BIN_FLAT", "params": {"nlist": 128}, "metric_type": "JACCARD"}, + "BIN_IVF_FLAT": {"index_type": "BIN_IVF_FLAT", "params": {"nlist": 128}, + "metric_type": "JACCARD"} + } diff --git a/tests/restful_client/config/log_config.py b/tests/restful_client/config/log_config.py new file mode 100644 index 0000000000000000000000000000000000000000..d3e3e30d07d93a93ecf01bda1a2024f3f91a5339 --- /dev/null +++ b/tests/restful_client/config/log_config.py @@ -0,0 +1,44 @@ +import os + + +class LogConfig: + def __init__(self): + self.log_debug = "" + self.log_err = "" + self.log_info = "" + self.log_worker = "" + self.get_default_config() + + @staticmethod + def get_env_variable(var="CI_LOG_PATH"): + """ get log path for testing """ + try: + log_path = os.environ[var] + return str(log_path) + except Exception as e: + # now = datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + log_path = f"/tmp/ci_logs" + print("[get_env_variable] failed to get environment variables : %s, use default path : %s" % (str(e), log_path)) + return log_path + + @staticmethod + def create_path(log_path): + if not os.path.isdir(str(log_path)): + print("[create_path] folder(%s) is not exist." % log_path) + print("[create_path] create path now...") + os.makedirs(log_path) + + def get_default_config(self): + """ Make sure the path exists """ + log_dir = self.get_env_variable() + self.log_debug = "%s/ci_test_log.debug" % log_dir + self.log_info = "%s/ci_test_log.log" % log_dir + self.log_err = "%s/ci_test_log.err" % log_dir + work_log = os.environ.get('PYTEST_XDIST_WORKER') + if work_log is not None: + self.log_worker = f'{log_dir}/{work_log}.log' + + self.create_path(log_dir) + + +log_config = LogConfig() diff --git a/tests/restful_client/conftest.py b/tests/restful_client/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..08f2511e92a02e5f6c2fb930421858095ca3bf21 --- /dev/null +++ b/tests/restful_client/conftest.py @@ -0,0 +1,49 @@ +import pytest +import common.common_func as cf +from check.param_check import ip_check, number_check +from config.log_config import log_config +from utils.util_log import test_log as log +from common.common_func import param_info + + +def pytest_addoption(parser): + parser.addoption("--host", action="store", default="127.0.0.1", help="Milvus host") + parser.addoption("--port", action="store", default="9091", help="Milvus http port") + parser.addoption('--clean_log', action='store_true', default=False, help="clean log before testing") + + +@pytest.fixture +def host(request): + return request.config.getoption("--host") + + +@pytest.fixture +def port(request): + return request.config.getoption("--port") + + +@pytest.fixture +def clean_log(request): + return request.config.getoption("--clean_log") + + +@pytest.fixture(scope="session", autouse=True) +def initialize_env(request): + """ clean log before testing """ + host = request.config.getoption("--host") + port = request.config.getoption("--port") + clean_log = request.config.getoption("--clean_log") + + + """ params check """ + assert ip_check(host) and number_check(port) + + """ modify log files """ + file_path_list = [log_config.log_debug, log_config.log_info, log_config.log_err] + if log_config.log_worker != "": + file_path_list.append(log_config.log_worker) + cf.modify_file(file_path_list=file_path_list, is_modify=clean_log) + + log.info("#" * 80) + log.info("[initialize_milvus] Log cleaned up, start testing...") + param_info.prepare_param_info(host, port) diff --git a/tests/restful_client/models/__init__.py b/tests/restful_client/models/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8d4899ce64ace886e67b59f05ebc6c886a03ecb7 --- /dev/null +++ b/tests/restful_client/models/__init__.py @@ -0,0 +1,3 @@ +# generated by datamodel-codegen: +# filename: openapi.json +# timestamp: 2022-12-08T02:46:08+00:00 diff --git a/tests/restful_client/models/common.py b/tests/restful_client/models/common.py new file mode 100644 index 0000000000000000000000000000000000000000..a4c33e484fb5e519a7cd983da3f6f70bdcaa306e --- /dev/null +++ b/tests/restful_client/models/common.py @@ -0,0 +1,28 @@ +# generated by datamodel-codegen: +# filename: openapi.json +# timestamp: 2022-12-08T02:46:08+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class KeyDataPair(BaseModel): + data: Optional[List[int]] = None + key: Optional[str] = None + + +class KeyValuePair(BaseModel): + key: Optional[str] = Field(None, example='dim') + value: Optional[str] = Field(None, example='128') + + +class MsgBase(BaseModel): + msg_type: Optional[int] = Field(None, description='Not useful for now') + + +class Status(BaseModel): + error_code: Optional[int] = None + reason: Optional[str] = None diff --git a/tests/restful_client/models/milvus.py b/tests/restful_client/models/milvus.py new file mode 100644 index 0000000000000000000000000000000000000000..25613b425866382faff0c944f70f92bcf5ef6483 --- /dev/null +++ b/tests/restful_client/models/milvus.py @@ -0,0 +1,138 @@ +# generated by datamodel-codegen: +# filename: openapi.json +# timestamp: 2022-12-08T02:46:08+00:00 + +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + +from models import common, schema + + +class DescribeCollectionRequest(BaseModel): + collection_name: Optional[str] = None + collectionID: Optional[int] = Field( + None, description='The collection ID you want to describe' + ) + time_stamp: Optional[int] = Field( + None, + description='If time_stamp is not zero, will describe collection success when time_stamp >= created collection timestamp, otherwise will throw error.', + ) + + +class DropCollectionRequest(BaseModel): + collection_name: Optional[str] = Field( + None, description='The unique collection name in milvus.(Required)' + ) + + +class FieldData(BaseModel): + field: Optional[List] = None + field_id: Optional[int] = None + field_name: Optional[str] = None + type: Optional[int] = Field( + None, + description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",', + ) + + +class GetCollectionStatisticsRequest(BaseModel): + collection_name: Optional[str] = Field( + None, description='The collection name you want get statistics' + ) + + +class HasCollectionRequest(BaseModel): + collection_name: Optional[str] = Field( + None, description='The unique collection name in milvus.(Required)' + ) + time_stamp: Optional[int] = Field( + None, + description='If time_stamp is not zero, will return true when time_stamp >= created collection timestamp, otherwise will return false.', + ) + + +class InsertRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = None + db_name: Optional[str] = None + fields_data: Optional[List[FieldData]] = None + hash_keys: Optional[List[int]] = None + num_rows: Optional[int] = None + partition_name: Optional[str] = None + + +class LoadCollectionRequest(BaseModel): + collection_name: Optional[str] = Field( + None, description='The collection name you want to load' + ) + replica_number: Optional[int] = Field( + None, description='The replica number to load, default by 1' + ) + + +class ReleaseCollectionRequest(BaseModel): + collection_name: Optional[str] = Field( + None, description='The collection name you want to release' + ) + + +class ShowCollectionsRequest(BaseModel): + collection_names: Optional[List[str]] = Field( + None, + description="When type is InMemory, will return these collection's inMemory_percentages.(Optional)", + ) + type: Optional[int] = Field( + None, + description='Decide return Loaded collections or All collections(Optional)', + ) + + +class VectorIDs(BaseModel): + collection_name: Optional[str] = None + field_name: Optional[str] = None + id_array: Optional[List[int]] = None + partition_names: Optional[List[str]] = None + + +class VectorsArray(BaseModel): + binary_vectors: Optional[List[int]] = Field( + None, + description='Vectors is an array of binary vector divided by given dim. Disabled when IDs is set', + ) + dim: Optional[int] = Field( + None, description='Dim of vectors or binary_vectors, not needed when use ids' + ) + ids: Optional[VectorIDs] = None + vectors: Optional[List[float]] = Field( + None, + description='Vectors is an array of vector divided by given dim. Disabled when ids or binary_vectors is set', + ) + + +class CalcDistanceRequest(BaseModel): + base: Optional[common.MsgBase] = None + op_left: Optional[VectorsArray] = None + op_right: Optional[VectorsArray] = None + params: Optional[List[common.KeyValuePair]] = None + + +class CreateCollectionRequest(BaseModel): + collection_name: str = Field( + ..., + description='The unique collection name in milvus.(Required)', + example='book', + ) + consistency_level: int = Field( + ..., + description='The consistency level that the collection used, modification is not supported now.\n"Strong": 0,\n"Session": 1,\n"Bounded": 2,\n"Eventually": 3,\n"Customized": 4,', + example=1, + ) + schema_: schema.CollectionSchema = Field(..., alias='schema') + shards_num: Optional[int] = Field( + None, + description='Once set, no modification is allowed (Optional)\nhttps://github.com/milvus-io/milvus/issues/6690', + example=1, + ) diff --git a/tests/restful_client/models/schema.py b/tests/restful_client/models/schema.py new file mode 100644 index 0000000000000000000000000000000000000000..c66c8ac642c48a6b21adef8ed6dc650ab7ae8ab1 --- /dev/null +++ b/tests/restful_client/models/schema.py @@ -0,0 +1,72 @@ +# generated by datamodel-codegen: +# filename: openapi.json +# timestamp: 2022-12-08T02:46:08+00:00 + +from __future__ import annotations + +from typing import Any, List, Optional + +from pydantic import BaseModel, Field + +from models import common + + +class FieldData(BaseModel): + field: Optional[Any] = Field( + None, + description='Types that are assignable to Field:\n\t*FieldData_Scalars\n\t*FieldData_Vectors', + ) + field_id: Optional[int] = None + field_name: Optional[str] = None + type: Optional[int] = Field( + None, + description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",', + ) + + +class FieldSchema(BaseModel): + autoID: Optional[bool] = None + data_type: int = Field( + ..., + description='0: "None",\n1: "Bool",\n2: "Int8",\n3: "Int16",\n4: "Int32",\n5: "Int64",\n10: "Float",\n11: "Double",\n20: "String",\n21: "VarChar",\n100: "BinaryVector",\n101: "FloatVector",', + example=101, + ) + description: Optional[str] = Field( + None, example='embedded vector of book introduction' + ) + fieldID: Optional[int] = None + index_params: Optional[List[common.KeyValuePair]] = None + is_primary_key: Optional[bool] = Field(None, example=False) + name: str = Field(..., example='book_intro') + type_params: Optional[List[common.KeyValuePair]] = None + + +class IDs(BaseModel): + idField: Optional[Any] = Field( + None, + description='Types that are assignable to IdField:\n\t*IDs_IntId\n\t*IDs_StrId', + ) + + +class LongArray(BaseModel): + data: Optional[List[int]] = None + + +class SearchResultData(BaseModel): + fields_data: Optional[List[FieldData]] = None + ids: Optional[IDs] = None + num_queries: Optional[int] = None + scores: Optional[List[float]] = None + top_k: Optional[int] = None + topks: Optional[List[int]] = None + + +class CollectionSchema(BaseModel): + autoID: Optional[bool] = Field( + None, + description='deprecated later, keep compatible with c++ part now', + example=False, + ) + description: Optional[str] = Field(None, example='Test book search') + fields: Optional[List[FieldSchema]] = None + name: str = Field(..., example='book') diff --git a/tests/restful_client/models/server.py b/tests/restful_client/models/server.py new file mode 100644 index 0000000000000000000000000000000000000000..57f7d06fe2ddf0a5f8d182237f7352e5b657f264 --- /dev/null +++ b/tests/restful_client/models/server.py @@ -0,0 +1,587 @@ +# generated by datamodel-codegen: +# filename: openapi.json +# timestamp: 2022-12-08T02:46:08+00:00 + +from __future__ import annotations + +from typing import Any, Dict, List, Optional + +from pydantic import BaseModel, Field + +from models import common, schema + + +class AlterAliasRequest(BaseModel): + alias: Optional[str] = None + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = None + db_name: Optional[str] = None + + +class BoolResponse(BaseModel): + status: Optional[common.Status] = None + value: Optional[bool] = None + + +class CalcDistanceResults(BaseModel): + array: Optional[Any] = Field( + None, + description='num(op_left)*num(op_right) distance values, "HAMMIN" return integer distance\n\nTypes that are assignable to Array:\n\t*CalcDistanceResults_IntDist\n\t*CalcDistanceResults_FloatDist', + ) + status: Optional[common.Status] = None + + +class CompactionMergeInfo(BaseModel): + sources: Optional[List[int]] = None + target: Optional[int] = None + + +class CreateAliasRequest(BaseModel): + alias: Optional[str] = None + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = None + db_name: Optional[str] = None + + +class CreateCredentialRequest(BaseModel): + base: Optional[common.MsgBase] = None + created_utc_timestamps: Optional[int] = Field(None, description='create time') + modified_utc_timestamps: Optional[int] = Field(None, description='modify time') + password: Optional[str] = Field(None, description='ciphertext password') + username: Optional[str] = Field(None, description='username') + + +class CreateIndexRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The particular collection name you want to create index.' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + extra_params: Optional[List[common.KeyValuePair]] = Field( + None, + description='Support keys: index_type,metric_type, params. Different index_type may has different params.', + ) + field_name: Optional[str] = Field( + None, description='The vector field name in this particular collection' + ) + index_name: Optional[str] = Field( + None, + description="Version before 2.0.2 doesn't contain index_name, we use default index name.", + ) + + +class CreatePartitionRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_name: Optional[str] = Field( + None, description='The partition name you want to create.' + ) + + +class DeleteCredentialRequest(BaseModel): + base: Optional[common.MsgBase] = None + username: Optional[str] = Field(None, description='Not useful for now') + + +class DeleteRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = None + db_name: Optional[str] = None + expr: Optional[str] = None + hash_keys: Optional[List[int]] = None + partition_name: Optional[str] = None + + +class DescribeIndexRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The particular collection name in Milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + field_name: Optional[str] = Field( + None, description='The vector field name in this particular collection' + ) + index_name: Optional[str] = Field( + None, description='No need to set up for now @2021.06.30' + ) + + +class DropAliasRequest(BaseModel): + alias: Optional[str] = None + base: Optional[common.MsgBase] = None + db_name: Optional[str] = None + + +class DropIndexRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field(None, description='must') + db_name: Optional[str] = None + field_name: Optional[str] = None + index_name: Optional[str] = Field( + None, description='No need to set up for now @2021.06.30' + ) + + +class DropPartitionRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_name: Optional[str] = Field( + None, description='The partition name you want to drop' + ) + + +class FlushRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_names: Optional[List[str]] = None + db_name: Optional[str] = None + + +class FlushResponse(BaseModel): + coll_segIDs: Optional[Dict[str, schema.LongArray]] = None + db_name: Optional[str] = None + status: Optional[common.Status] = None + + +class GetCollectionStatisticsResponse(BaseModel): + stats: Optional[List[common.KeyValuePair]] = Field( + None, description='Collection statistics data' + ) + status: Optional[common.Status] = None + + +class GetCompactionPlansRequest(BaseModel): + compactionID: Optional[int] = None + + +class GetCompactionPlansResponse(BaseModel): + mergeInfos: Optional[List[CompactionMergeInfo]] = None + state: Optional[int] = None + status: Optional[common.Status] = None + + +class GetCompactionStateRequest(BaseModel): + compactionID: Optional[int] = None + + +class GetCompactionStateResponse(BaseModel): + completedPlanNo: Optional[int] = None + executingPlanNo: Optional[int] = None + state: Optional[int] = None + status: Optional[common.Status] = None + timeoutPlanNo: Optional[int] = None + + +class GetFlushStateRequest(BaseModel): + segmentIDs: Optional[List[int]] = Field(None, alias='segment_ids') + + +class GetFlushStateResponse(BaseModel): + flushed: Optional[bool] = None + status: Optional[common.Status] = None + + +class GetImportStateRequest(BaseModel): + task: Optional[int] = Field(None, description='id of an import task') + + +class GetImportStateResponse(BaseModel): + id: Optional[int] = Field(None, description='id of an import task') + id_list: Optional[List[int]] = Field( + None, description='auto generated ids if the primary key is autoid' + ) + infos: Optional[List[common.KeyValuePair]] = Field( + None, + description='more informations about the task, progress percent, file path, failed reason, etc.', + ) + row_count: Optional[int] = Field( + None, + description='if the task is finished, this value is how many rows are imported. if the task is not finished, this value is how many rows are parsed. return 0 if failed.', + ) + state: Optional[int] = Field( + None, description='is this import task finished or not' + ) + status: Optional[common.Status] = None + + +class GetIndexBuildProgressRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + field_name: Optional[str] = Field( + None, description='The vector field name in this collection' + ) + index_name: Optional[str] = Field(None, description='Not useful for now') + + +class GetIndexBuildProgressResponse(BaseModel): + indexed_rows: Optional[int] = None + status: Optional[common.Status] = None + total_rows: Optional[int] = None + + +class GetIndexStateRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field(None, description='must') + db_name: Optional[str] = None + field_name: Optional[str] = None + index_name: Optional[str] = Field( + None, description='No need to set up for now @2021.06.30' + ) + + +class GetIndexStateResponse(BaseModel): + fail_reason: Optional[str] = None + state: Optional[int] = None + status: Optional[common.Status] = None + + +class GetMetricsRequest(BaseModel): + base: Optional[common.MsgBase] = None + request: Optional[str] = Field(None, description='request is of jsonic format') + + +class GetMetricsResponse(BaseModel): + component_name: Optional[str] = Field( + None, description='metrics from which component' + ) + response: Optional[str] = Field(None, description='response is of jsonic format') + status: Optional[common.Status] = None + + +class GetPartitionStatisticsRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_name: Optional[str] = Field( + None, description='The partition name you want to collect statistics' + ) + + +class GetPartitionStatisticsResponse(BaseModel): + stats: Optional[List[common.KeyValuePair]] = None + status: Optional[common.Status] = None + + +class GetPersistentSegmentInfoRequest(BaseModel): + base: Optional[common.MsgBase] = None + collectionName: Optional[str] = Field(None, alias="collection_name", description='must') + dbName: Optional[str] = Field(None, alias="db_name") + + +class GetQuerySegmentInfoRequest(BaseModel): + base: Optional[common.MsgBase] = None + collectionName: Optional[str] = Field(None, alias="collection_name", description='must') + dbName: Optional[str] = Field(None, alias="db_name") + + +class GetReplicasRequest(BaseModel): + base: Optional[common.MsgBase] = None + collectionID: Optional[int] = Field(None, alias="collection_id") + with_shard_nodes: Optional[bool] = None + + +class HasPartitionRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_name: Optional[str] = Field( + None, description='The partition name you want to check' + ) + + +class ImportRequest(BaseModel): + channel_names: Optional[List[str]] = Field( + None, description='channel names for the collection' + ) + collection_name: Optional[str] = Field(None, description='target collection') + files: Optional[List[str]] = Field(None, description='file paths to be imported') + options: Optional[List[common.KeyValuePair]] = Field( + None, description='import options, bucket, etc.' + ) + partition_name: Optional[str] = Field(None, description='target partition') + row_based: Optional[bool] = Field( + None, description='the file is row-based or column-based' + ) + + +class ImportResponse(BaseModel): + status: Optional[common.Status] = None + tasks: Optional[List[int]] = Field(None, description='id array of import tasks') + + +class IndexDescription(BaseModel): + field_name: Optional[str] = Field(None, description='The vector field name') + index_name: Optional[str] = Field(None, description='Index name') + indexID: Optional[int] = Field(None, description='Index id') + params: Optional[List[common.KeyValuePair]] = Field( + None, description='Will return index_type, metric_type, params(like nlist).' + ) + + +class ListCredUsersRequest(BaseModel): + base: Optional[common.MsgBase] = None + + +class ListCredUsersResponse(BaseModel): + status: Optional[common.Status] = None + usernames: Optional[List[str]] = Field(None, description='username array') + + +class ListImportTasksRequest(BaseModel): + pass + + +class ListImportTasksResponse(BaseModel): + status: Optional[common.Status] = None + tasks: Optional[List[GetImportStateResponse]] = Field( + None, description='list of all import tasks' + ) + + +class LoadBalanceRequest(BaseModel): + base: Optional[common.MsgBase] = None + collectionName: Optional[str] = None + dst_nodeIDs: Optional[List[int]] = None + sealed_segmentIDs: Optional[List[int]] = None + src_nodeID: Optional[int] = None + + +class LoadPartitionsRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_names: Optional[List[str]] = Field( + None, description='The partition names you want to load' + ) + replica_number: Optional[int] = Field( + None, description='The replicas number you would load, 1 by default' + ) + + +class ManualCompactionRequest(BaseModel): + collectionID: Optional[int] = None + timetravel: Optional[int] = None + + +class ManualCompactionResponse(BaseModel): + compactionID: Optional[int] = None + status: Optional[common.Status] = None + + +class PersistentSegmentInfo(BaseModel): + collectionID: Optional[int] = None + num_rows: Optional[int] = None + partitionID: Optional[int] = None + segmentID: Optional[int] = None + state: Optional[int] = None + + +class QueryRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = None + db_name: Optional[str] = None + expr: Optional[str] = None + guarantee_timestamp: Optional[int] = Field(None, description='guarantee_timestamp') + output_fields: Optional[List[str]] = None + partition_names: Optional[List[str]] = None + travel_timestamp: Optional[int] = None + + +class QueryResults(BaseModel): + collection_name: Optional[str] = None + fields_data: Optional[List[schema.FieldData]] = None + status: Optional[common.Status] = None + + +class QuerySegmentInfo(BaseModel): + collectionID: Optional[int] = None + index_name: Optional[str] = None + indexID: Optional[int] = None + mem_size: Optional[int] = None + nodeID: Optional[int] = None + num_rows: Optional[int] = None + partitionID: Optional[int] = None + segmentID: Optional[int] = None + state: Optional[int] = None + + +class ReleasePartitionsRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, description='The collection name in milvus' + ) + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_names: Optional[List[str]] = Field( + None, description='The partition names you want to release' + ) + + +class SearchRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field(None, description='must') + db_name: Optional[str] = None + dsl: Optional[str] = Field(None, description='must') + dsl_type: Optional[int] = Field(None, description='must') + guarantee_timestamp: Optional[int] = Field(None, description='guarantee_timestamp') + output_fields: Optional[List[str]] = None + partition_names: Optional[List[str]] = Field(None, description='must') + placeholder_group: Optional[List[int]] = Field( + None, description='serialized `PlaceholderGroup`' + ) + search_params: Optional[List[common.KeyValuePair]] = Field(None, description='must') + travel_timestamp: Optional[int] = None + + +class SearchResults(BaseModel): + collection_name: Optional[str] = None + results: Optional[schema.SearchResultData] = None + status: Optional[common.Status] = None + + +class ShardReplica(BaseModel): + dm_channel_name: Optional[str] = None + leader_addr: Optional[str] = Field(None, description='IP:port') + leaderID: Optional[int] = None + node_ids: Optional[List[int]] = Field( + None, + description='optional, DO NOT save it in meta, set it only for GetReplicas()\nif with_shard_nodes is true', + ) + + +class ShowCollectionsResponse(BaseModel): + collection_ids: Optional[List[int]] = Field(None, description='Collection Id array') + collection_names: Optional[List[str]] = Field( + None, description='Collection name array' + ) + created_timestamps: Optional[List[int]] = Field( + None, description='Hybrid timestamps in milvus' + ) + created_utc_timestamps: Optional[List[int]] = Field( + None, description='The utc timestamp calculated by created_timestamp' + ) + inMemory_percentages: Optional[List[int]] = Field( + None, description='Load percentage on querynode when type is InMemory' + ) + status: Optional[common.Status] = None + + +class ShowPartitionsRequest(BaseModel): + base: Optional[common.MsgBase] = None + collection_name: Optional[str] = Field( + None, + description='The collection name you want to describe, you can pass collection_name or collectionID', + ) + collectionID: Optional[int] = Field(None, description='The collection id in milvus') + db_name: Optional[str] = Field(None, description='Not useful for now') + partition_names: Optional[List[str]] = Field( + None, + description="When type is InMemory, will return these patitions' inMemory_percentages.(Optional)", + ) + type: Optional[int] = Field( + None, description='Decide return Loaded partitions or All partitions(Optional)' + ) + + +class ShowPartitionsResponse(BaseModel): + created_timestamps: Optional[List[int]] = Field( + None, description='All hybrid timestamps' + ) + created_utc_timestamps: Optional[List[int]] = Field( + None, description='All utc timestamps calculated by created_timestamps' + ) + inMemory_percentages: Optional[List[int]] = Field( + None, description='Load percentage on querynode' + ) + partition_names: Optional[List[str]] = Field( + None, description='All partition names for this collection' + ) + partitionIDs: Optional[List[int]] = Field( + None, description='All partition ids for this collection' + ) + status: Optional[common.Status] = None + + +class UpdateCredentialRequest(BaseModel): + base: Optional[common.MsgBase] = None + created_utc_timestamps: Optional[int] = Field(None, description='create time') + modified_utc_timestamps: Optional[int] = Field(None, description='modify time') + newPassword: Optional[str] = Field(None, description='new password') + oldPassword: Optional[str] = Field(None, description='old password') + username: Optional[str] = Field(None, description='username') + + +class DescribeCollectionResponse(BaseModel): + aliases: Optional[List[str]] = Field( + None, description='The aliases of this collection' + ) + collection_name: Optional[str] = Field(None, description='The collection name') + collectionID: Optional[int] = Field(None, description='The collection id') + consistency_level: Optional[int] = Field( + None, + description='The consistency level that the collection used, modification is not supported now.', + ) + created_timestamp: Optional[int] = Field( + None, description='Hybrid timestamp in milvus' + ) + created_utc_timestamp: Optional[int] = Field( + None, description='The utc timestamp calculated by created_timestamp' + ) + physical_channel_names: Optional[List[str]] = Field( + None, description='System design related, users should not perceive' + ) + schema_: Optional[schema.CollectionSchema] = Field(None, alias='schema') + shards_num: Optional[int] = Field(None, description='The shards number you set.') + start_positions: Optional[List[common.KeyDataPair]] = Field( + None, description='The message ID/posititon when collection is created' + ) + status: Optional[common.Status] = None + virtual_channel_names: Optional[List[str]] = Field( + None, description='System design related, users should not perceive' + ) + + +class DescribeIndexResponse(BaseModel): + index_descriptions: Optional[List[IndexDescription]] = Field( + None, + description='All index informations, for now only return tha latest index you created for the collection.', + ) + status: Optional[common.Status] = None + + +class GetPersistentSegmentInfoResponse(BaseModel): + infos: Optional[List[PersistentSegmentInfo]] = None + status: Optional[common.Status] = None + + +class GetQuerySegmentInfoResponse(BaseModel): + infos: Optional[List[QuerySegmentInfo]] = None + status: Optional[common.Status] = None + + +class ReplicaInfo(BaseModel): + collectionID: Optional[int] = None + node_ids: Optional[List[int]] = Field(None, description='include leaders') + partition_ids: Optional[List[int]] = Field( + None, description='empty indicates to load collection' + ) + replicaID: Optional[int] = None + shard_replicas: Optional[List[ShardReplica]] = None + + +class GetReplicasResponse(BaseModel): + replicas: Optional[List[ReplicaInfo]] = None + status: Optional[common.Status] = None diff --git a/tests/restful_client/pytest.ini b/tests/restful_client/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..da7be902081c17ac75c26766148da10ed85fe5df --- /dev/null +++ b/tests/restful_client/pytest.ini @@ -0,0 +1,12 @@ +[pytest] + + +addopts = --host 10.101.178.131 --html=/tmp/ci_logs/report.html --self-contained-html -v +# python3 -W ignore -m pytest + +log_format = [%(asctime)s - %(levelname)s - %(name)s]: %(message)s (%(filename)s:%(lineno)s) +log_date_format = %Y-%m-%d %H:%M:%S + + +filterwarnings = + ignore::DeprecationWarning \ No newline at end of file diff --git a/tests/restful_client/requirements.txt b/tests/restful_client/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..da51bc9912dcb86cf5a8623f62e2bcf7474af70f --- /dev/null +++ b/tests/restful_client/requirements.txt @@ -0,0 +1,2 @@ +decorest~=0.1.0 +pydantic~=1.10.2 \ No newline at end of file diff --git a/tests/restful_client/testcases/test_e2e.py b/tests/restful_client/testcases/test_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..525ad9913e71be6e0aec3d73841f845ddf627104 --- /dev/null +++ b/tests/restful_client/testcases/test_e2e.py @@ -0,0 +1,49 @@ +from time import sleep +from common import common_type as ct +from common import common_func as cf +from base.client_base import TestBase +from utils.util_log import test_log as log + + +class TestDefault(TestBase): + + def test_e2e(self): + collection_name, schema = self.init_collection() + nb = ct.default_nb + # insert + res = self.entity_service.insert(collection_name=collection_name, fields_data=cf.gen_fields_data(schema, nb=nb), + num_rows=nb) + log.info(f"insert {nb} rows into collection {collection_name}, response: {res}") + # flush + res = self.entity_service.flush(collection_names=[collection_name]) + log.info(f"flush collection {collection_name}, response: {res}") + # create index for vector field + vector_field_name = cf.get_vector_field(schema) + vector_index_params = cf.gen_index_params(index_type="HNSW") + res = self.index_service.create_index(collection_name=collection_name, field_name=vector_field_name, + extra_params=vector_index_params) + log.info(f"create index for vector field {vector_field_name}, response: {res}") + # load + res = self.collection_service.load_collection(collection_name=collection_name) + log.info(f"load collection {collection_name}, response: {res}") + + sleep(5) + # search + vectors = cf.gen_vectors(nq=ct.default_nq, schema=schema) + res = self.entity_service.search(collection_name=collection_name, vectors=vectors, + output_fields=[ct.default_int64_field_name], + search_params=cf.gen_search_params()) + log.info(f"search collection {collection_name}, response: {res}") + + # hybrid search + res = self.entity_service.search(collection_name=collection_name, vectors=vectors, + output_fields=[ct.default_int64_field_name], + search_params=cf.gen_search_params(), + dsl=ct.default_dsl) + log.info(f"hybrid search collection {collection_name}, response: {res}") + # query + res = self.entity_service.query(collection_name=collection_name, expr=ct.default_expr) + + log.info(f"query collection {collection_name}, response: {res}") + + diff --git a/tests/restful_client/utils/util_log.py b/tests/restful_client/utils/util_log.py new file mode 100644 index 0000000000000000000000000000000000000000..4743f30b9d5a41c81f7b943bc83645aa7e8d3d4b --- /dev/null +++ b/tests/restful_client/utils/util_log.py @@ -0,0 +1,57 @@ +import logging +import sys + +from config.log_config import log_config + + +class TestLog: + def __init__(self, logger, log_debug, log_file, log_err, log_worker): + self.logger = logger + self.log_debug = log_debug + self.log_file = log_file + self.log_err = log_err + self.log_worker = log_worker + + self.log = logging.getLogger(self.logger) + self.log.setLevel(logging.DEBUG) + + try: + formatter = logging.Formatter("[%(asctime)s - %(levelname)s - %(name)s]: " + "%(message)s (%(filename)s:%(lineno)s)") + # [%(process)s] process NO. + dh = logging.FileHandler(self.log_debug) + dh.setLevel(logging.DEBUG) + dh.setFormatter(formatter) + self.log.addHandler(dh) + + fh = logging.FileHandler(self.log_file) + fh.setLevel(logging.INFO) + fh.setFormatter(formatter) + self.log.addHandler(fh) + + eh = logging.FileHandler(self.log_err) + eh.setLevel(logging.ERROR) + eh.setFormatter(formatter) + self.log.addHandler(eh) + + if self.log_worker != "": + wh = logging.FileHandler(self.log_worker) + wh.setLevel(logging.DEBUG) + wh.setFormatter(formatter) + self.log.addHandler(wh) + + ch = logging.StreamHandler(sys.stdout) + ch.setLevel(logging.DEBUG) + ch.setFormatter(formatter) + # self.log.addHandler(ch) + + except Exception as e: + print("Can not use %s or %s or %s to log. error : %s" % (log_debug, log_file, log_err, str(e))) + + +"""All modules share this unified log""" +log_debug = log_config.log_debug +log_info = log_config.log_info +log_err = log_config.log_err +log_worker = log_config.log_worker +test_log = TestLog('ci_test', log_debug, log_info, log_err, log_worker).log diff --git a/tests/restful_client/utils/util_wrapper.py b/tests/restful_client/utils/util_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..d122054c6846a45f8b279f655206d6743f3908b6 --- /dev/null +++ b/tests/restful_client/utils/util_wrapper.py @@ -0,0 +1,38 @@ +import time +from datetime import datetime +import functools +from utils.util_log import test_log as log + +DEFAULT_FMT = '[{start_time}] [{elapsed:0.8f}s] {collection_name} {func_name} -> {res!r}' + + +def trace(fmt=DEFAULT_FMT, prefix='test', flag=True): + def decorate(func): + @functools.wraps(func) + def inner_wrapper(*args, **kwargs): + # args[0] is an instance of ApiCollectionWrapper class + flag = args[0].active_trace + if flag: + start_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + t0 = time.perf_counter() + res, result = func(*args, **kwargs) + elapsed = time.perf_counter() - t0 + end_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') + func_name = func.__name__ + collection_name = args[0].collection.name + # arg_lst = [repr(arg) for arg in args[1:]][:100] + # arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items()) + # arg_str = ', '.join(arg_lst)[:200] + + log_str = f"[{prefix}]" + fmt.format(**locals()) + # TODO: add report function in this place, like uploading to influxdb + # it is better a async way to do this, in case of blocking the request processing + log.info(log_str) + return res, result + else: + res, result = func(*args, **kwargs) + return res, result + + return inner_wrapper + + return decorate