[오픈소스] Stryker

[Stryker] 새로 올라온 이슈: Proposal: break out of infinite loops using a hit counter #3023

지기_ 2021. 10. 18. 23:21

Implement the hit counter for Jest 🃏. This is to prevent infinite loops from resulting in a timeout, see #3023.

Jest 🃏에 대한 적중 카운터를 구현하십시오. 이는 무한 루프로 인해 시간 초과가 발생하는 것을 방지하기 위한 것입니다(#3023 참조).

 

Proposal: break out of infinite loops using a hit counter · Issue #3023 · stryker-mutator/stryker-js

Is your feature request related to a problem? Please describe. Stryker might be mutating your code to be an infinite loop. There is no way around that, because of something called the Halting probl...

github.com

 

 

 

#3023의 내용

 

Stryker가 코드를 무한 루프로 변경하고 있을 수 있습니다. 이를 우회할 방법이 없습니다. 정지 문제라고 하는 문제 때문입니다. 예를 들어:

let goOn = true;
let i = 0;
while(goOn) {
  if(activeMutant(1)) {
    i--;   // 👈 mutant that creates an infinite loop
  }
  else { 
    i++; 
  }
  if(i >10){
    goOn = false;
  }
}

현재 StrykerJS가 무한 루프를 처리하는 방법은 작업자 프로세스를 종료하고 새 프로세스를 시작하는 것입니다. 이것은 작동하지만 특히 webpack 또는 기타 JIT 빌드 단계가 포함될 때 매우 비쌀 수 있습니다. Angular 프로젝트를 예로 들어 보겠습니다. 새로운 테스트 러너 작업자 프로세스를 시작하는 데 1분이 걸리는 것은 대규모 프로젝트의 경우 매우 '정상'입니다(새 브라우저도 시작된다는 사실 제외).

 


🤝
원하는 솔루션 설명

StrykerJS가 코드를 계측할 때 돌연변이가 실행된 횟수를 계산하는 적중 카운터를 추가할 수 있습니다. 절대 제한에 도달하면 "hitCountLimitReached" 플래그를 설정하고 오류를 발생시킬 수 있습니다. 그런 다음 테스트 러너에서 플래그의 존재를 확인하고 돌연변이를 타임아웃 결과로 보고할 수 있습니다.

 

function activeMutant(id) {
  if (id === global.__stryker__.activeMutant) {
    if (global.__stryker__.hitCount  !== undefined || ++global.__stryker__.hitCount > global.__stryker__hitCountLimit) {
      throw new Error('Mutant hit count reached');
    }
    return true;
  }
  return false;
}

보시다시피 돌연변이의 hitCount가 미리 결정된 제한을 초과하면 Stryker에서 오류가 발생합니다. 미리 결정된 제한은 일반(변경되지 않은) 실행 중 적중 횟수를 기반으로 할 수 있으며, 이는 테스트 실행 중에 결정할 수 있습니다.

 

hitLimit(mutant) = hits(mutant) & hitLimitFactor


hitLimitFactor가 높을수록 무한 루프에서 벗어나는 데 더 오래 걸리지만 hitLimitFactor를 너무 낮게 설정하면 거짓 부정이 발생할 수 있습니다. 우리는 안전한 공장(즉, 100)으로 시작하여 그것이 우리에게 무엇을 주는지 볼 수 있습니다.
TimeouMutantRunResult에 Reason 필드를 추가할 수도 있습니다. Reason는 테스트 러너가 "Hit limit of x에 도달함"(또는 이와 유사한 것)으로 제공할 수 있습니다. 그렇게 하면 HTML 보고서에서 이러한 결과를 식별할 수 있습니다.

 

이 접근 방식에는 몇 가지 단점이 있습니다.

- 보편적인 "하드 리미트"를 결정하는 것은 어렵습니다. 일부 작업(이미지 처리, 성능 테스트 등)의 경우 하드 제한 1000은 너무 낮습니다. 따라서 이 제한을 구성할 수 있어야 한다고 생각합니다. --mutantHitLimit? 이 값을 0으로 설정하면 적중 제한을 해제할 수 있습니다. @hcoles는 Proposal에서 훌륭한 아이디어를 내놓았습니다. 히트 카운터 #3023(comment)을 사용하여 무한 루프를 탈출합니다. 제한은 일반 적중 횟수를 기준으로 결정되며, 이는 테스트 실행(초기 테스트 실행) 중에 계산할 수 있습니다. 이것은 --mutantHitLimit에 대한 필요성을 완전히 제거합니다. 미래에 구성 가능한 --mutantHitLimitFactory를 도입할 수 있지만 지금은 예를 들어 100의 하드 팩토리를 사용할 수 있습니다(안전한 측면에서)
- 테스트에서 오류가 제대로 처리되지 않을 수 있습니다. 처리되지 않은 예외 또는 처리되지 않은 약속이 거부될 수 있습니다. 이러한 핸들러를 추가하는 것에 대해 생각하거나 각 테스트 실행기가 이미 적절하게 처리하는지 조사할 수 있습니다.
- 코드에서 오류를 포착할 수 있습니다. 이것은 예기치 않은 결과를 초래할 수 있습니다
    루프 내부에서 오류가 포착되면 루프가 깨지지 않고 무한대로 유지됩니다.  
    다른 오류가 처리될 것으로 예상되는 루프 외부에서 오류가 발견된 경우. 이로 인해 테스트가 실패할 수 있지만(테스트 실행자가 시간 초과로 변환해야 함) 여전히 예기치 않은 동작입니다.
- 테스트 실행자는 더 복잡해질 것입니다(이 코드 중 일부를 @stryker-mutator/utils로 이동할 수 있지만):
전역 "hitCountLimitReached" 플래그를 기반으로 시간 초과가 있을 때 결정합니다.
hitCount 및 hitCountLimit 변수를 모두 재설정하십시오. 재설정하는 것을 잊어버리면 위음성이 발생합니다.

 

보시다시피, 이는 최선의 접근 방식일 수 있으며 Stryker는 무한 루프를 추가로 처리하기 위해 항상 현재 동작이 필요합니다. 특히 테스트 로직에서 콜백을 사용하는 것과 관련하여 이 구현에서 처리되지 않는 시간 초과가 여전히 있다는 점에 유의하십시오. 예를 들어:

// foo.js
function maybeEmitFooEvent(a, b) {
  if (activeMutant(1) ? a > b : a < b) {
     emitter.emit('foo');
  }
}

// foo.spec.js
it('should raise event', (done) => {
  emitter.on('foo', done);
  maybeEmitFooEvent(1, 2);
});

 


 

댓글

(1) 나는 몇 년 동안 이것을 구현하려고 했습니다. 고정된 하드 제한보다는 항상 변경되지 않은 상태에서 메서드에 대한 각 테스트의 적중 횟수를 기록하고, 돌연변이 프로브가 현재 테스트의 것보다 'x'배 더 많은 코드를 적중하면 오류가 발생하도록 계획했습니다.

=>원래 테스트 케이스의 2 배 3 배 같은 수치를  limit으로 설정해서 그것을 넘어가면 에러 발생시키면서 멈추는 것으로

 

(2) 나는 그것이 나쁜 생각이 아니라고 생각하며 테스트당 적중 횟수를 계산하는 것이 아마도 IMO를 수행하는 더 안전한 방법일 것입니다. 조회수를 사용자가 구성할 수 있도록 하는 것이 얼마나 중요한지 사용자가 알아야 하기 때문에 그다지 가치가 있는지 확신할 수 없습니다. 오류가 포착된 위치에 대한 우려가 유효하다고 생각합니다. 스레드가 종료되고 그런 식으로 표시될 수 있도록 무한 루프를 나타내는 스레드 부모에게 신호를 보내는 것이 더 나을까요? 개인적으로 나는 여전히 우리가 이에 대한 장기적인 해결책으로 이야기한 하트비트 아이디어의 팬입니다.

=> 사용자가 값을 입력하는 것이 더 좋고, 에러가 발생했을 때 에러가 일어난 위치를 알려주는 것이 좋을 것 같다는 의견.

 

(3) 2의 대댓

" 조회수를 사용자가 구성할 수 있도록 하는 것이 얼마나 중요한지 사용자가 알아야 하기 때문에 그다지 가치가 있는지 확신할 수 없습니다." 에 대해:

여전히 사용자가 요소를 구성하도록 허용할 수 있습니다. 예를 들어 계수가 2이면 테스트 실행에서보다 2배 더 많이 사용됩니다. 그러나 필요하지 않아야 하기 때문에 configurable(config 파일로 조절하는 것)하게 만들고 싶지 않다는 데 동의합니다. 우리는 내부 요인을 높은 숫자로 만들어 안전한 쪽으로 기울일 수 있습니다(예: 요인 100).'

=> 안전한 limit을 거는 것이 configurable하게 하는 것보다 낫다는 생각

 

"오류가 포착된 위치에 대한 우려가 유효하다고 생각합니다. 스레드가 종료되고 그런 식으로 표시될 수 있도록 무한 루프를 나타내는 신호를 스레드 부모에게 보내는 것이 더 나을까요?"에 대해:

흠, 흥미롭군요. 그러나 테스트는 브라우저(karma) 내부 또는 VM(jest) 내부에서 실행될 수 있습니다. 따라서 이 구현은 테스트 실행기에 의존해야 합니다.

 

"스레드가 종료되고 그런 식으로 표시될 수 있도록 무한 루프를 나타내는 신호를 스레드 부모에게 보내는 것이 더 나을까요?"에 대해:

"스레드 부모"와 "스레드를 죽일 수 있음"은 무엇을 의미합니까? JS는 단일 스레드입니다. 테스트 실행기 프로세스는 일반적으로 브라우저(단일 스레드) 내에서 테스트를 실행하는 karma를 제외하고 스레드에서 테스트를 실행합니다. 루프를 벗어나지 않고 이를 복구할 수 있는 유일한 방법은 테스트 러너 작업자를 종료하고 새 프로세스를 시작하는 것입니다. "신호"를 보내면 평소보다 빨리 종료하고 다시 시작할 수 있습니다. 시간 초과 타이머가 만료될 때까지 기다릴 필요가 없기 때문이지만 JIT가 많은 프로젝트에는 여전히 비용이 많이 듭니다.

 

따라서 "throw Error" 접근 방식을 선호합니다. Instrumenter에 baked할 수 있으므로 테스트 실행자의 작업이 줄어듭니다. 테스트 러너에서 돌연변이 실행은 다음과 같습니다.

class FooTestRunner {
  async runMutant(options) {
    // Prepare hit counter stuff
    global.__stryker__.hitCount = 0;
    global.__stryker__.hitCountLimitReached = false;
    global.__stryker__.hitCountLimit = options.hitCountLimit;
    
     // Switch active mutant
    global.__stryker__.activeMutant = options.activeMutant.id;
    
    // Do the actual test run
    const result = await underlyingTestRunner.run();
    
    // See if it was a timeout
    if (global.__stryker__.hitCountLimitReached) {
      return { state: 'timeout' };
    } else {
      return result
    }
  }
}

좋은 점은 테스트 실행자가 적중 카운터 항목을 완전히 무시하도록 선택할 수 있다는 것입니다. 테스트 러너가 구현하지 않더라도 지금처럼 시간 초과를 처리할 수 있습니다.

 

(4)

정말 좋아보이네요. 우리가 stryker를 프로파일링할 때 프로세스를 다시 시작하는 데 비용이 많이 들기 때문에 성능에 실제로 도움이 될 수 있음을 이미 알아차렸습니다. childProcesses를 다시 시작하면 이론적으로 실행 중에 무언가를 엉망으로 만들 수 있기 때문에 안정성에도 도움이 될 수 있다고 생각합니다. :)