Azure Native Qumulo 이제 EU, 영국 및 캐나다에서 사용 가능 – 자세히 알아보기

동전을 던지는 것처럼 쉽게 100% 코드 커버리지 만들기

작성자 :

우리는 Qumulo에서의 테스트에 집착합니다.

XNUMX주마다 새로운 버전의 분산 확장 파일 시스템을 출시한다는 것은 코드베이스에 대한 모든 커밋에 버그가 없다는 확신이 있어야 한다는 것을 의미합니다. 이를 위해 우리는 회귀를 도입하지 않았는지 확인하기 위해 모든 빌드를 타격하는 수만 개의 단위 테스트, 수천 개의 통합 테스트 및 수백 개의 전체 시스템 테스트를 보유하고 있습니다.

이러한 종류의 테스트가 중요하지만 우리는 더 나아가고 싶었습니다. 우리는 코드의 특정 부분을 통해 가능한 모든 경로를 실행하는 방법을 원했기 때문에 어떤 일이 있어도 코드의 동작이 정확하고 시스템의 불변성이 유지되는지 확인할 수 있습니다. 이 문제에 대한 전통적인 접근 방식은 코드 커버리지 보고서를 수동으로 검사하고 코드의 각 분기를 실행하기 위한 개별 테스트를 만드는 것일 수 있지만 이는 코드 커버리지를 검사하는 사람이 필요하고 종종 복잡하고 때로는 복잡한 테스트를 포함하기 때문에 취약합니다. 드러나지 않은 코드를 실행합니다. 또는 코드의 모든 분기를 확률적으로 탐색하는 퍼즈 테스트를 작성할 수 있지만 이는 잘 작성하기 어렵고 본질적으로 모든 흥미로운 경우를 탐색하지 못할 수 있습니다. 우리는 우리가 더 잘할 수 있다고 생각했습니다.

동기를 부여하는 예

바이너리 형식에서 구조체로 금귤 주문을 역직렬화하는 함수가 있다고 상상해보십시오. kumquat_order</var/www/wordpress>:

struct kumquat_order { 문자 이름[100]; 무부호 수량; }; int deserialize_kumquat_order(struct istream *input, struct kumquat_order *out) { int error_code = istream_read(input, out->quantity, sizeof out->quantity); if (error_code != 0) return error_code; 부울 익명; error_code = istream_read(input, &anonymous, sizeof anonymous); if (error_code != 0) return error_code; if (anonymous) strcpy(order->name, "anonymous"); 그렇지 않으면 istream_read(input, out->name, sizeof out->name); 0을 반환합니다. }

이 함수의 불변성 중 하나는 istream에 대한 호출이 오류를 반환하면 함수도 오류를 반환해야 한다는 것입니다. 보시다시피 주문에서 이름을 읽을 때 해당 불변이 위반되었습니다. 여기서 오류 코드는 istream_read()</var/www/wordpress> is completely ignored:

istream_read(input, out->name, sizeof out->name);</var/www/wordpress>

이 경우 이 버그를 발견하기 위해 단위 테스트를 작성하는 것이 상상할 수 없을 정도로 어렵지는 않지만 kumquat_order에 더 많은 필드를 추가하고 바이너리 형식에 더 복잡해짐에 따라 개별 단위 테스트를 작성하여 확인하기가 점점 더 어려워질 것입니다. 우리의 불변성이며 이러한 종류의 버그로부터 보호합니다. 우리가 정말 원하는 것은 호출되는 모든 지점에서 결정론적으로 오류를 반환할 수 있는 istream 인터페이스의 구현입니다.

rseq 소개

이러한 철저한 커버리지 테스트를 수행하기 위해 우리는 "무작위 시퀀스"를 의미하는 rseq라는 구성 요소를 만들었습니다(약간 잘못된 이름이지만 rseq에는 "무작위"라는 것이 없습니다). rseq의 기본 인터페이스는 매우 간단합니다.

구조체 rseq; bool rseq_flip_coin(struct rseq *); bool rseq_next_simulation(struct rseq *);

rseq는 테스트 시뮬레이션 개념을 기반으로 합니다. 테스트가 시뮬레이션에서 결정 세트의 일부가 되고자 하는 결정 지점에 도달할 때마다 다음을 호출합니다. rseq_flip_coin()</var/www/wordpress>, which returns a boolean that the test can use to influence the behavior of this simulation of the test. At the end of each simulation, the test calls rseq_next_simulation()</var/www/wordpress>, which will return false when there are no more simulations with unique paths through the decision space. Let’s look at a simple example:

struct rseq *rseq = rseq_new(); do { printf("%c", rseq_flip_coin(rseq) ? 't' : 'f'); printf("%c", rseq_flip_coin(rseq) ? 't' : 'f'); printf("%c,", rseq_flip_coin(rseq) ? 't' : 'f'); } 동안(rseq_next_simulation(rseq));

이 코드의 출력은 전체 결정 조합 세트입니다.

fff, fft, ftf, ftt, tff, tft, ttf, ttt,</var/www/wordpress>

중요한 것은 rseq는 rseq_flip_coin()</var/www/wordpress> are conditional. For example, if we only make the second call to rseq_flip_coin()</var/www/wordpress> if the first call returns true:

struct rseq *rseq = rseq_new(); do { if (rseq_flip_coin(rseq)) { printf("a"); printf("%c", rseq_flip_coin(rseq) ? 'b' : 'c'); } 그렇지 않으면 printf("d"); printf("%c,", rseq_flip_coin(rseq) ? 'e' : 'f'); } 동안(rseq_next_simulation(rseq));

출력은 다음과 같습니다.

df, de, acf, ace, abf, abe</var/www/wordpress>

이진법이 아닌 결정(예: XNUMX가지 옵션 중에서 선택)은 동전 던지기를 사용하여 구현할 수 있지만 rseq는 다음과 같은 기능을 유용하게 제공합니다.

unsigned rseq_roll_die(struct rseq *, unsigned sides);</var/www/wordpress>

rseq 내부

알다시피 rseq는 결정 공간에서 일종의 "거짓 우선" 검색을 수행합니다. 비교적 간단한 방법으로 이를 수행합니다. 반환할 결정의 스크립트를 추적하고 동전 던지기 결정을 나타내는 부울 벡터와 스크립트의 현재 위치를 추적하는 커서로 구현합니다.

전화로 rseq_flip_coin()</var/www/wordpress> are made, the decision at the current cursor is returned. If the cursor is at the end of the script when the call is made, we append a false decision to the script and return false from the rseq_flip_coin()</var/www/wordpress> call.

인셀덤 공식 판매점인 rseq_next_simulation()</var/www/wordpress> is called, we trim trailing true decisions from the end of the script, which represent decision paths that have been fully explored, and flip the last false decision in the script to true and rseq_next_simulation() returns true. If the script is empty after trimming trailing true decisions, the decision tree has been fully explored and rseq_next_simulation()</var/www/wordpress> returns false.

영어가 명확하지 않은 경우, 우리는 repl.it에서 rseq의 기본 대화식 Python 버전, 작동 방식을 이해하는 데 도움이 되도록 가지고 놀 수 있습니다.

철저하게 테스트 kumquat_order</var/www/wordpress> deserializer

Qumulo에서 rseq를 사용하는 일반적인 방법 중 하나는 테스트 중인 시스템(SUT)에 종속성 주입되는 인터페이스의 테스트 이중 구현입니다.

위의 예제로 돌아가서, 우리가 원하는 것은 오류를 반환하거나 실제 작업을 수행하는 istream의 다른 구현으로 떨어지는 istream 인터페이스 버전입니다. 나중에 명확해질 이유 때문에 우리는 또한 이 rseq 인식 istream에 대한 일부 호출이 오류를 반환했는지 알고 싶을 것입니다. 다음은 약간의 Qumulo C 인터페이스 마법을 사용하는 구현 예입니다.

struct rseq_error_istream { a_implements(istream); // Qumulo 매직! 구조체 rseq *rseq; 부울 반환_오류; // false로 초기화됨 struct istream *delegate; }; int rseq_error_istream_read(struct rseq_error_istream *self, void *data, unsigned c) { if (rseq_flip_coin(self->rseq)) { self->returned_error = true; -1 반환; } 그렇지 않으면 istream_read(self->delegate, data, c)를 반환합니다. }

여기서 rseq를 사용하면 rseq_next_simulation()</var/www/wordpress> returns false, every call into istream_read made by the SUT will have returned an error in at least one of the simulations. Using such an implementation of istream, we can now write a test which will fail when run against our kumquat_order</var/www/wordpress> deserializer:

struct rseq *rseq = rseq_new(); do { // 픽스처 설정 struct file_istream *order_istream = file_istream_new('test_order')); struct rseq_error_istream *rseq_istream = rseq_error_istream_new( rseq, istream_from_file_istream(order_istream)); // 연습 SUT struct kumquat_order order; int error = deserialize_kumquat_order( istream_from_rseq_error_istream(rseq_istream), &order); // if (rseq_istream->returned_error)를 확인합니다. assert(error == -1); else assert(kumquat_order_is_correct(&order)); // 픽스처 분해 rseq_error_istream_free(rseq_istream); file_istream_free(order_istream); } 동안 (rseq_next_simulation());

이 테스트의 정말 멋진 점은 테스트 방법에 대해 완전히 독립적이라는 것입니다. deserialize_kumquat_order()</var/www/wordpress> uses the istream. As we add more complexity to the deserialization code, this test will continue to exercise every possible error path through the SUT, verifying our invariant without us having to write a new test for each new path. We could even extend the invariant to say that no calls should be made into the istream after it returns an error with relative ease by making the following modification:

int rseq_error_istream_read(struct rseq_error_istream *self, void *data, unsigned c) { assert(!self->returned_error); if (rseq_flip_coin(self->rseq)) { self->returned_error = true; -1 반환; } 그렇지 않으면 istream_read(self->delegate, data, c)를 반환합니다. }

rseq의 다른 용도

Fail-fast 검증은 Qumulo에서 rseq를 많이 사용하는 방법 중 하나입니다. rseq로 수행한 다른 흥미로운 작업은 다음과 같습니다.

  • 클러스터의 노드에 동시에 전달되는 RPC의 가능한 모든 주문 시뮬레이션
  • 스레드가 양보할 때 실행할 다음 스레드를 선택하기 위해 스케줄러에서 rseq를 사용하여 사용자 공간 협력 스레드의 가능한 모든 인터리빙을 시뮬레이션합니다(예, 우리는 자체 사용자 공간 스레딩 라이브러리를 작성했습니다!).
  • 복잡한 시스템의 가능한 모든 상태를 시뮬레이션하고 코드가 해당 상태에서 진행될 수 있음을 보여줍니다(예: 분산 트랜잭션 복구).
    rseq 함정

rseq는 매우 유용하지만 함정도 있습니다.

  • rseq는 각 시뮬레이션에 대해 결정적인 rseq_flip_coin()에 대한 호출 순서에 의존하기 때문에 비결정론에 적합하지 않습니다. 예를 들어 SUT가 rand()를 사용하여 결국 rseq를 호출하는 호출을 만들지 여부를 결정하면 빠르게 혼란스러워집니다. 이 문제를 해결하기 위해 rand()를 호출하지만 테스트에서 실행될 때 rseq를 사용하는 테스트 double로 대체할 수 있는 프로덕션 구현이 있는 종속성 주입 인터페이스에 이 임의성을 캡슐화합니다.
  • 마찬가지로 다중 스레드 환경에서 rseq를 사용하는 것은 위험합니다. 시뮬레이션 중에 여러 스레드가 동일한 rseq 인스턴스에서 작동할 수 있는 경우 이는 rseq에 대한 호출 순서에 비결정성을 도입합니다. 이것과 다른 많은 이유로 우리는 가능할 때마다 스레드를 생성하는 대신 비동기 호출 패턴을 사용하여 대부분의 코드를 단일 스레드로 유지하기 위해 매우 열심히 노력합니다.
  • SUT가 복잡할 때 rseq는 폭발적인 상태가 되어 결정 공간을 철저하게 검색하기 위해 실행해야 하는 시뮬레이션 수가 기하급수적으로 증가할 수 있습니다. 우리는 실제로 이것으로 많은 문제를 겪지 않았지만 운 좋게도 rseq는 우리가 원할 경우 작업자가 각각 검색 공간의 일부를 차지하는 분산 방식으로 실행하는 데 적합합니다.

관련 게시물

위쪽으로 스크롤