Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Travis CI është një shërbim ueb i shpërndarë për ndërtimin dhe testimin e softuerit që përdor GitHub si host të kodit burimor. Përveç skenarëve të mësipërm të funksionimit, ju mund të shtoni tuajin falë opsioneve të gjera të konfigurimit. Në këtë artikull ne do të konfigurojmë Travis CI për të punuar me PVS-Studio duke përdorur shembullin e kodit PPSSPP.

Paraqitje

Travis C.I. është një shërbim ueb për ndërtimin dhe testimin e softuerit. Zakonisht përdoret në lidhje me praktikat e integrimit të vazhdueshëm.

PPSSPP - Emulatori i konsolës së lojërave PSP. Programi është në gjendje të imitojë nisjen e ndonjë lojë nga imazhet e diskut të destinuara për Sony PSP. Programi u publikua më 1 nëntor 2012. PPSSPP është licencuar sipas GPL v2. Çdokush mund të bëjë përmirësime në kodi burim i projektit.

Studio PVS — një analizues i kodit statik për kërkimin e gabimeve dhe dobësive të mundshme në kodin e programit. Në këtë artikull, për një ndryshim, ne do të hapim PVS-Studio jo në nivel lokal në makinën e zhvilluesit, por në cloud dhe do të kërkojmë gabime në PPSSPP.

Vendosja e Travis CI

Do të na duhet një depo në GitHub, ku ndodhet projekti që na nevojitet, si dhe një çelës për PVS-Studio (mund të merrni çelësi i provës ose falas për projektet me burim të hapur).

Le të shkojmë në sit Travis C.I.. Pas autorizimit duke përdorur llogarinë tuaj GitHub, ne do të shohim një listë të depove:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Për provën, kam pirun PPSSPP.

Ne aktivizojmë depon që duam të mbledhim:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Për momentin, Travis CI nuk mund të ndërtojë projektin tonë sepse nuk ka udhëzime për ndërtimin. Pra, është koha për konfigurim.

Gjatë analizës, disa variabla do të jenë të dobishëm për ne, për shembull, çelësi për PVS-Studio, i cili do të ishte i padëshirueshëm të specifikohej në skedarin e konfigurimit. Pra, le të shtojmë variablat e mjedisit duke përdorur cilësimet e ndërtimit në Travis CI:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Ne do të duhet:

  • PVS_USERNAME - emri i përdoruesit
  • PVS_KEY - çelësi
  • MAIL_USER - email që do të përdoret për të dërguar raportin
  • MAIL_PASSWORD - fjalëkalimi i emailit

Dy të fundit janë opsionale. Këto do të përdoren për të dërguar rezultatet me postë. Nëse dëshironi ta shpërndani raportin në një mënyrë tjetër, nuk keni nevojë t'i tregoni ato.

Pra, ne kemi shtuar variablat e mjedisit që na duhen:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Tani le të krijojmë një skedar .travis.yml dhe vendoseni në rrënjën e projektit. PPSSPP tashmë kishte një skedar konfigurimi për Travis CI, megjithatë, ai ishte shumë i madh dhe plotësisht i papërshtatshëm për shembullin, kështu që na duhej ta thjeshtonim shumë dhe të linim vetëm elementët bazë.

Së pari, le të tregojmë gjuhën, versionin e Ubuntu Linux që duam të përdorim në makinën virtuale dhe paketat e nevojshme për ndërtimin:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

Të gjitha paketat e listuara nevojiten ekskluzivisht për PPSSPP.

Tani ne tregojmë matricën e montimit:

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PPSSPP_BUILD_TYPE=Linux PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"
      env: PPSSPP_BUILD_TYPE=Linux

Pak më shumë rreth seksionit matricë. Në Travis CI, ekzistojnë dy mënyra për të krijuar opsione ndërtimi: e para është të specifikoni një listë të përpiluesve, llojeve të sistemit operativ, variablave të mjedisit, etj., pas së cilës krijohet një matricë e të gjitha kombinimeve të mundshme; i dyti është një tregues i qartë i matricës. Sigurisht, ju mund t'i kombinoni këto dy qasje dhe të shtoni një rast unik, ose, përkundrazi, ta përjashtoni atë duke përdorur seksionin përjashtoj. Ju mund të lexoni më shumë rreth kësaj në Dokumentacioni Travis CI.

Gjithçka që mbetet është të jepni udhëzime të montimit të projektit specifik:

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Travis CI ju lejon të shtoni komandat tuaja për faza të ndryshme të jetës së një makine virtuale. Seksioni para_instalimit ekzekutuar përpara instalimit të paketave. Pastaj instaloj, e cila pason instalimin e paketave nga lista shtesa.apttë cilën e treguam më lart. Vetë asambleja zhvillohet në dorëshkrim. Nëse gjithçka shkoi mirë, atëherë ne e gjejmë veten brenda pas_suksesit (është në këtë seksion që ne do të kryejmë analizën statike). Këto nuk janë të gjitha hapat që mund të modifikohen, nëse keni nevojë për më shumë, atëherë duhet të shikoni Dokumentacioni Travis CI.

Për lehtësinë e leximit, komandat u vendosën në një skenar të veçantë .travis.sh, i cili vendoset në rrënjën e projektit.

Pra kemi skedarin e mëposhtëm .travis.yml:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Përpara se të instalojmë paketat, ne do të përditësojmë nënmodulet. Kjo është e nevojshme për të ndërtuar PPSSPP. Le të shtojmë funksionin e parë në .travis.sh (vini re zgjerimin):

travis_before_install() {
  git submodule update --init --recursive
}

Tani kemi ardhur drejtpërdrejt te konfigurimi i lëshimit automatik të PVS-Studio në Travis CI. Së pari duhet të instalojmë paketën PVS-Studio në sistem:

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}

Në fillim të funksionit travis_install ne instalojmë kompajlerët që na duhen duke përdorur variablat e mjedisit. Atëherë nëse ndryshorja $PVS_ANALYZE ruan vlerën Po (e kemi treguar në seksion zili gjatë konfigurimit të matricës së ndërtimit), ne instalojmë paketën pvs-studio. Përveç kësaj, tregohen edhe paketat libio-socket-ssl-perl и libnet-ssleay-perl, megjithatë, ato kërkohen për rezultatet e postimit, kështu që nuk janë të nevojshme nëse keni zgjedhur një metodë tjetër për dorëzimin e raportit tuaj.

Funksion shkarkim_ekstrakt shkarkon dhe shpaketon arkivin e specifikuar:

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

Është koha për të bashkuar projektin. Kjo ndodh në seksion dorëshkrim:

travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}

Në fakt, ky është një konfigurim origjinal i thjeshtuar, me përjashtim të këtyre rreshtave:

if [ "$PVS_ANALYZE" = "Yes" ]; then
  CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
fi

Në këtë seksion të kodit kemi vendosur për cmake flamur për eksportimin e komandave të përpilimit. Kjo është e nevojshme për një analizues të kodit statik. Mund të lexoni më shumë rreth kësaj në artikullin "Si të ekzekutoni PVS-Studio në Linux dhe macOS".

Nëse asambleja ishte e suksesshme, atëherë arrijmë pas_suksesit, ku kryejmë analiza statike:

travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}

Le të hedhim një vështrim më të afërt në rreshtat e mëposhtëm:

pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                -o PVS-Studio-${CC}.log 
                                --disableLicenseExpirationCheck
plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html

Rreshti i parë gjeneron një skedar licence nga emri i përdoruesit dhe çelësi që specifikuam që në fillim kur konfiguruam variablat e mjedisit Travis CI.

Rreshti i dytë fillon drejtpërdrejt analizën. Flamuri -j cakton numrin e fijeve për analizë, flamur -l tregon licencën, flamurin -o përcakton skedarin për nxjerrjen e regjistrave dhe flamurin -çaktivizoni kontrollin e skadimit të licencës kërkohet për versionet e provës, pasi si parazgjedhje pvs-studio-analizator do të paralajmërojë përdoruesin se licenca është gati të skadojë. Për të parandaluar që kjo të ndodhë, mund të specifikoni këtë flamur.

Skedari i regjistrit përmban rezultate të papërpunuara që nuk mund të lexohen pa konvertim, kështu që së pari duhet ta bëni skedarin të lexueshëm. Le t'i kalojmë shkrimet plog-konverter, dhe dalja është një skedar html.

Në këtë shembull, vendosa të dërgoj raporte me postë duke përdorur komandën Dërgoni një email.

Si rezultat, morëm skedarin e mëposhtëm .travis.sh:

#/bin/bash

travis_before_install() {
  git submodule update --init --recursive
}

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}
travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}
travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}
set -e
set -x

$1;

Tani është koha për të shtyrë ndryshimet në depo git, pas së cilës Travis CI do të ekzekutojë automatikisht ndërtimin. Klikoni në "ppsspp" për të shkuar te raportet e ndërtimit:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Ne do të shohim një përmbledhje të ndërtimit aktual:

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull
Nëse ndërtimi përfundon me sukses, do të marrim një email me rezultatet e analizës statike. Sigurisht, dërgimi me postë nuk është mënyra e vetme për të marrë një raport. Ju mund të zgjidhni çdo metodë zbatimi. Por është e rëndësishme të mbani mend se pasi të përfundojë ndërtimi, nuk do të jetë e mundur të aksesoni skedarët e makinës virtuale.

Përmbledhja e gabimit

Kemi përfunduar me sukses pjesën më të vështirë. Tani le të sigurohemi që të gjitha përpjekjet tona ia vlejnë. Le të shohim disa pika interesante nga raporti i analizës statike që më erdhi me postë (jo më kot e tregova).

Optimizimi i rrezikshëm

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  memset( &ctx, 0, sizeof( sha1_context ) );
}

Paralajmërim PVS-Studio: V597 Përpiluesi mund të fshijë thirrjen e funksionit 'memset', e cila përdoret për të pastruar bufferin 'sum'. Funksioni RtlSecureZeroMemory() duhet të përdoret për të fshirë të dhënat private. sha1.cpp 325

Kjo pjesë e kodit ndodhet në modulin e hashimit të sigurt, megjithatë, ai përmban një defekt serioz sigurie (CWE-14). Le të shohim listën e montimit që krijohet gjatë përpilimit të versionit Debug:

; Line 355
  mov r8d, 20
  xor edx, edx
  lea rcx, QWORD PTR sum$[rsp]
  call memset
; Line 356

Gjithçka është në rregull dhe funksioni memeset është ekzekutuar, duke mbishkruar kështu të dhëna të rëndësishme në RAM, megjithatë, mos u gëzoni ende. Le të shohim listën e montimit të versionit të lëshimit me optimizim:

; 354  :
; 355  :  memset( sum, 0, sizeof( sum ) );
; 356  :}

Siç mund të shihet nga lista, përpiluesi e injoroi thirrjen memeset. Kjo për faktin se në funksion sha1 pas thirrjes memeset nuk ka më referencë për strukturën CTX. Prandaj, përpiluesi nuk sheh asnjë pikë në humbjen e kohës së procesorit duke mbishkruar kujtesën që nuk përdoret në të ardhmen. Ju mund ta rregulloni këtë duke përdorur funksionin RtlSecureZeroMemory ose një të ngjashme asaj.

saktë:

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  RtlSecureZeroMemory(&ctx, sizeof( sha1_context ) );
} 

Krahasimi i panevojshëm

static u32 sceAudioOutputPannedBlocking
             (u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    if (leftvol >= 0) {
      chans[chan].leftVolume = leftvol;
    }
    if (rightvol >= 0) {
      chans[chan].rightVolume = rightvol;
    }
    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

Paralajmërim PVS-Studio: V547 Shprehja 'leftvol >= 0' është gjithmonë e vërtetë. sceAudio.cpp 120

Kushtojini vëmendje degës tjetër për të parën if. Kodi do të ekzekutohet vetëm nëse të gjitha kushtet leftvol > 0xFFFF || rightvol > 0xFFFF || volum i majtë < 0 || volum i drejtë < 0 do të rezultojë e rreme. Prandaj, marrim pohimet e mëposhtme, të cilat do të jenë të vërteta për degën tjetër: volum i majtë <= 0xFFFF, rightvol <= 0xFFFF, volum i majtë >= 0 и volum i drejtë >= 0. Vini re dy deklaratat e fundit. A ka kuptim të kontrollohet se cili është kushti i nevojshëm për ekzekutimin e kësaj pjese të kodit?

Kështu që ne mund t'i heqim me siguri këto deklarata të kushtëzuara:

static u32 sceAudioOutputPannedBlocking
(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    chans[chan].leftVolume = leftvol;
    chans[chan].rightVolume = rightvol;

    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

Një tjetër skenar. Ekziston një lloj gabimi i fshehur pas këtyre kushteve të tepërta. Ndoshta ata nuk kontrolluan atë që kërkohej.

Ctrl+C Ctrl+V Kundërshton

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfData) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

V501 Ka nën-shprehje identike '!Memory::IsValidAddress(psmfData)' në të majtë dhe në të djathtë të '||' operatori. scePsmf.cpp 703

Kushtojini vëmendje çekut brenda if. A nuk mendoni se është e çuditshme që ne kontrollojmë nëse adresa është e vlefshme? psmfData, dy herë më shumë? Pra, kjo më duket e çuditshme... Në fakt, kjo është, sigurisht, një gabim shtypi dhe ideja ishte të kontrolloheshin të dy parametrat e hyrjes.

Opsioni i duhur:

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfStruct) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

Ndryshore e harruar

extern void ud_translate_att(
  int size = 0;
  ....
  if (size == 8) {
    ud_asmprintf(u, "b");
  } else if (size == 16) {
    ud_asmprintf(u, "w");
  } else if (size == 64) {
    ud_asmprintf(u, "q");
  }
  ....
}

Paralajmërim PVS-Studio: V547 Shprehja 'madhësia == 8' është gjithmonë e gabuar. syn-att.c 195

Ky gabim ndodhet në dosje ext, kështu që nuk është vërtet e rëndësishme për projektin, por defekti u gjet përpara se ta vura re, kështu që vendosa ta lë. Në fund të fundit, ky artikull nuk ka të bëjë me rishikimin e gabimeve, por me integrimin me Travis CI, dhe nuk u krye asnjë konfigurim i analizuesit.

Ndryshore madhësi inicializohet nga një konstante, megjithatë, ajo nuk përdoret fare në kod, deri te operatori if, e cila, natyrisht, jep i rremë gjatë kontrollit të kushteve, sepse, siç kujtojmë, madhësi e barabartë me zero. Kontrollet e mëvonshme gjithashtu nuk kanë kuptim.

Me sa duket, autori i fragmentit të kodit ka harruar të mbishkruajë variablin madhësi përpara se.

Stop

Këtu ndoshta do të përfundojmë me gabimet. Qëllimi i këtij artikulli është të demonstrojë punën e PVS-Studio së bashku me Travis CI, dhe jo të analizojë projektin sa më plotësisht të jetë e mundur. Nëse doni gabime më të mëdha dhe më të bukura, gjithmonë mund t'i admironi ato këtu :).

Përfundim

Përdorimi i shërbimeve në internet për të ndërtuar projekte së bashku me praktikën e analizës në rritje ju lejon të gjeni shumë probleme menjëherë pas bashkimit të kodit. Megjithatë, një ndërtim mund të mos jetë i mjaftueshëm, kështu që vendosja e testimit së bashku me analizën statike do të përmirësojë ndjeshëm cilësinë e kodit.

Lidhje të dobishme

Si të konfiguroni PVS-Studio në Travis CI duke përdorur emulatorin PSP si shembull

Nëse dëshironi ta ndani këtë artikull me një audiencë anglishtfolëse, ju lutemi përdorni lidhjen e përkthimit: Maxim Zvyagintsev. Si të konfiguroni PVS-Studio në Travis CI duke përdorur shembullin e emulatorit të konsolës së lojërave PSP.

Burimi: www.habr.com

Shto një koment