Привет, Хабр! Меня зовут Мария Иванищева, я работаю на ИТ-проектах Fix Price. Я расскажу о том, как мы автоматизировали процессы код-ревью в SAP.

Проблемы и риски

С ростом штата программистов производительность увеличивается далеко не всегда, ведь для этого нужно решить проблему с тем, что каждый программист пишет код по-своему. И чаще всего случается так, что пишет код один человек, а поддерживать его затем приходится другим. При этом написание кода может не отличаться качеством.

Применительно к SAP это означает еще большие проблемы, потому что SAP включает среды разработки, тестирования и продуктивный контур (будем называть его продуктив). И при переносе кода со среды разработки на тестирование и затем на продуктив могут возникнуть (и возникают) ошибки, которые приходится править, возвращая задачу разработчикам, или откатывать версию. Выбор, разумеется, не из лучших. 

Неприятности могут возникнуть и на работающем коде. Яркий пример: при увеличении объема обрабатываемых данных значительно увеличивается время работы программы или же вовсе происходит падение. В результате возникает острая потребность в оптимизации кода.

Но и это еще не всё: третья проблема связана с незначительными (на первый взгляд) правками, которые были внесены когда-то давно. Казалось бы — программа работает и работает, но ведь всегда есть риск, что при дальнейших манипуляциях с кодом программистам придется разбираться, что именно было изменено. выяснение особенностей кода может затянуться. Поэтому единственное разумное решение во всех этих случаях — выработка стандартов, целью которых является универсальность написания кода.

Что было сделано в первую очередь

Важнейшим шагом к созданию нормальных процессов код-ревью стало создание Регламента разработки на ABAP :

  • универсальность кода было решено поддерживать благодаря неймингу — правилам именования объектов разработки;

  • структурирование кода осуществляется за счет разбивки кода на блоки программ и вынесения часто используемых процедур в отдельно запускаемые  функции, что исключает дублирование.

Также перед выполнением работ по оптимизации у нас всегда составляется План оптимизации, который в обязательном порядке корректируют и согласуют синьоры.: составление этого документа помогает в дальнейшем избежать множества проблем, в том числе неправильного сбора данных или их потерю 

Вся документация хранится в JIRA и Confluence, где указаны проекты, номера задач и все описания работ по ним. Каждый запрос имеет правильное наименование, что обеспечивает эффективную связь запроса в SAP с документацией в JIRA.

Автоматизация код-ревью

Вот мы и добрались до главного. Приведенная выше схема хороша в плане исключения ошибок: ревьюеры смотрят и комментируют код в соответствии с утвержденными стандартами, разработчики — правят ошибки. Но эта схема не оптимальна, поскольку процесс Code Review при такой организации оказывается достаточно долгим, и время на задачу разработки увеличивается.

Поскольку процессы код-ревью алгоритмизуемы, их можно упростить. Для этого мы разделили код-ревью на несколько этапов:

  • транспорт,

  • документация,

  • нейминг,

  • модульность,

  • оптимизация.

И что касается процессов транспорта и нейминга, то они поддались автоматизации и на текущий момент выполняются программой.

Алгоритм проверки объектов мы также разбили на несколько блоков:

  • Выбираются все объекты запроса.

  • Обходится всё дерево используемых объектов, собирается полный список Z-объектов и расширений стандарта.

  • Проверяется статус переносов промежуточных запросов каждого объекта в целевую систему. При этом решается задача контроля изменений кода под разные задачи. То есть ни один объект не пропускается и не берутся в перенос разработки по другим задачам, которые еще не прошли проверку.

  • Находится разница кода с продуктивом и проверяется на нейминг в соответствии с регламентом.

  • Недостающие или избыточные объекты собираются в общий список и выводятся на монитор с описанием ошибок.

Теперь переходим к самому «вкусному» — собственно технической реализации всего описанного выше.

Как это реализовано

Поскольку всё собрано в одном запросе, и перенос изменений связан с рисками его транспорта , то проверяется именно запрос. Разумеется, запрос и/или все подзадачи должны быть деблокированы, то есть по ним не должно вестись никаких работ. 

Из конкретных требований: запрос должен содержать в первых символах номер заявки/задачи (через дефис) и перечень задач через запятую. Блоки списков задач по разным проектам разделяются точкой с запятой, а обязательное краткое описание запроса не должно включать точки с запятой (;), дефиса (-) и точки (.), поскольку эти знаки участвуют в синтаксисе самих запросов.

Вот примеры запросов:

  • HRS-1234 краткое описание

  • SMP-1234,2345,7654 еще одно краткое описание

  • SFP-7891, 1234; SD-7890, 1269, 1234 и снова краткое описание

Посмотрим на интерфейс проверки запросов на перенос:

Проверка объектов осуществляется через транзакцию Z_CR в системе разработки SAP BD1. Чек-боксы вынесены на экран выбора для проверки программистом. Так как транзакцией пользуются и консультанты, и администратор переносов, то эти пункты вариативны.

Далее мы определяем тип запроса: инструментальные средства или настройка. Перенос копий запрещен в продуктив и не обрабатывается программой проверки. В зависимости от этого собираем по нему данные. Затем определяем проект и номер задач по наименованию и получаем таблицу с проектами и номерам задач:

Номер задачи, к которой принадлежит запрос на перенос, определяется по наименованию запроса. Если по результатам проведенной автоматической проверки выведена пустая таблица, значит проверка пройдена успешно, и запрос не содержит ошибок:

В противном случае в таблице появится список обнаруженных ошибок запроса и сообщение о дальнейших действиях. В информационном сообщении выведено подробное указание, как поступать в зависимости от статуса ошибки, при этом ошибки указаны построчно для каждого объекта запроса:

Обработку результатов выполняет программист, запуская проверку после подготовки запроса. Далее найденные ошибки исправляются, после чего выполняется повторная проверка, и, при отсутствии ошибок (или их согласования руководителями групп), разработка передается на тестирование.

Также на скрине выше вы наверняка заметили, что ошибки помечены разным цветом:

  • красные требуют исправления и последующего согласования;

  • желтые требуют согласования руководителя проекта;

  • не помеченные цветом обозначают хардкод.

Теперь более подробно об обработке запросов.

Основной алгоритм обработки запросов через Z_CR в SAP BD1

В первую очередь определяем тип запроса и определяем по нему данные.

Hidden text
SELECT * INTO TABLE mt_e071
  	FROM e071
  	WHERE trkorr = mv_trkorr. - номер запроса

Удаляем такие объекты как: переводы, класс сообщений и сами сообщения, внутренние тексты программ, подзадачи запроса. 

Hidden text
	"Эти подсветим потом на выходе - версии проверять не надо
	DELETE mt_e071 WHERE object = 'CDAT'.  "Ведение кластера ракурса
	DELETE mt_e071 WHERE object = 'INDX'.  "Табличный индекс
	DELETE mt_e071 WHERE object = 'TABU'.  "Табличные данные
 
	IF mt_e071_tkor IS INITIAL.
  	MESSAGE 'Запрос не деблокирован или указан несуществующий'(001) TYPE 'E'.
	ENDIF.

Данные запроса настройки  будут выведены с желтым флагом, и их анализ в дальнейшем выполняться не будет

Теперь можно приступать к запросу инструментальных средств.

Идем по таблице объектов запроса:
LOOP AT mt_e071 INTO ls_e071.
  CLEAR: lt_env, lt_e071_fm.
      CLEAR ls_dat.
      ls_dat-epgmid    = ls_e071-pgmid.
      ls_dat-eobject   = ls_e071-object.
      ls_dat-eobj_name = ls_e071-obj_name.

Теперь ищем объекты внутри с помощью метода _search_objects_inside:

  _search_objects_inside( EXPORTING iv_obj_type    = ls_e071-object
                                        iv_object_name = ls_e071-obj_name
                              IMPORTING et_env         = lt_env  ).
      IF lines( lt_env ) = 0.
        CONTINUE.
      ENDIF.

Сравнение версий объектов с продуктивом:_check_versiya_object_prod

  _check_versiya_object_prod( EXPORTING it_e071 = lt_e071_fm
                                           is_dat  = ls_dat  ).

Проверка на расширение стандартных таблиц

 SELECT * INTO TABLE lt_dd03l FROM dd03l WHERE tabname = ls_tab
                                             AND fieldname LIKE '.INCL%'
                                             AND precfield LIKE 'Z%'.
…        
          ls_dat-light     = '@0A@'.  "RED
          ls_dat-comment   = 'Расширение станд.табл отлично от продуктива'.

_search_objects_inside - сбор используемых объектов внутри
DATA: ls_environment_selection TYPE  envi_types.
    ls_environment_selection-prog = 'X'. "Программы
    ls_environment_selection-fugr = 'X'. "Группы функций
    ls_environment_selection-ldba = ' '. "Логические базы данных
    ls_environment_selection-msag = ' '. "Классы сообщений
    ls_environment_selection-tran = ' '. "Транзакции
    ls_environment_selection-func = 'X'. "Функциональные модули
    ls_environment_selection-dial = ' '. "Диалоговые модули
    ls_environment_selection-tabl = 'X'. "Таблицы базы данных
    ls_environment_selection-shlp = 'X'. "Средства поиска
    ls_environment_selection-doma = 'X'. "Домены
ls_environment_selection-dtel = 'X'. "Элементы данных
    ls_environment_selection-view = ' '. "Курсоры
    ls_environment_selection-mcob = ' '. "Matchcode object
    ls_environment_selection-mcid = ' '. "Matchcode ID
    ls_environment_selection-para = ' '. "SET/GET параметры
    ls_environment_selection-conv = ' '. "Conversion exit
    ls_environment_selection-suso = ' '. "Объекты авторизации
    ls_environment_selection-type = 'X'. "Группы типов
    ls_environment_selection-ttyp = 'X'. "Типы таблиц
    ls_environment_selection-stru = 'X'. "Структуры
    ls_environment_selection-enqu = 'X'. "Объекты блокировок
    ls_environment_selection-sqlt = ' '. "SQL tables
    ls_environment_selection-clas = 'X'. "Классы
    ls_environment_selection-intf = 'X'. "Интерфейсы
    ls_environment_selection-udmo = ' '. "Data model
    ls_environment_selection-ueno = ' '. "Entity type
    ls_environment_selection-shi3 = ' '. "Area menu
    ls_environment_selection-cntx = ' '. "Context
    ls_environment_selection-ttab = 'X'. "Типы таблиц
“ или 
      

 “ls_environment_selection = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'.
 CALL FUNCTION 'REPOSITORY_ENVIRONMENT_SET_RFC'
        EXPORTING
          obj_type          = lv_obj_type "тип объекта(prog/clas/meth и др.)                                  
          environment_types = ls_environment_selection
          object_name       = ls_tn-obj_name "идентификатор объекта                           (название)
        TABLES
          environment       = lt_env.

… 
    DELETE lt_env WHERE type = 'MSAG'.  "класс сообщений
    DELETE lt_env WHERE type = 'MESS'.  "отдельное сообщение
    DELETE lt_env WHERE type = 'DOCU'.  "документация
    DELETE lt_env WHERE type = 'CDAT'.  "ведение кластера ракурса

    "удалить submit изнутри
    DELETE ct_env WHERE type = 'PROG' AND int_type = 'S'. 
    "Методы, типы, атрибуты, события, класса и т.д.  
      DELETE ct_env WHERE ( ( type = 'CLAS' OR type = 'INTF' )
                                 AND object+0(1) <> 'Z' )
                      OR ( type+0(1) = 'O'  AND encl_obj+0(1) <> 'Z' )
                      OR  ( type = 'DGT' AND  object+0(1) <> 'Z'). "тип из группы               
                                                                   типов  ( se11)

 В зависимости от типа объекта можно корректировать сбор проверяемых объектов и удалить стандартные объекты, объекты, которые используются в запросе.

Для объектов типа метод или функциональный метод сбор выше не подойдет. 

Собираем

METHOD _object_for_meth
  DATA: ls_methodid     TYPE seocpdkey.
    DATA: lt_split        TYPE TABLE OF sobj_name
        , ls_split        TYPE sobj_name
        , ls_tn           TYPE ty_s_tn
        .

    SPLIT iv_obj_name AT space INTO TABLE lt_split.

    DELETE lt_split WHERE table_line IS INITIAL.
 READ TABLE lt_split INTO ls_methodid-clsname  INDEX 1.
    READ TABLE lt_split INTO ls_methodid-cpdname  INDEX 2.

    "название программы запуска метода ( в SE38 )
    zclbs_service=>get_methnameprog(
      EXPORTING
        iv_clsname  =  ls_methodid-clsname   " название класса
        iv_cpdname  =  ls_methodid-cpdname   " название метода
      IMPORTING
        ev_progname =    ls_tn-obj_name " программа
    ).
*
    IF ls_tn-obj_name IS NOT INITIAL.
      ls_tn-obj_type   = 'METH'.
      ls_tn-flag_check = 'X'.
      APPEND ls_tn TO et_tn.
    ENDIF.

    "Вытаскиваем Public
    ls_tn-obj_name = ls_methodid-clsname  . "cl_oo_classname_service=>get_pubsec_name( clsname = ls_methodid-clsname ).
    ls_tn-obj_type = 'CPUB'.
    ls_tn-flag_check = ''.
    APPEND ls_tn TO et_tn.

    "Вытаскиваем Private
    ls_tn-obj_name = ls_methodid-clsname . "cl_oo_classname_service=>get_prisec_name( clsname =  ls_methodid-clsname ).
    ls_tn-obj_type = 'CPRI'.
    ls_tn-flag_check = ''.
    APPEND ls_tn TO et_tn.

    "Вытаскиваем Protecte
    ls_tn-obj_name = ls_methodid-clsname . "cl_oo_classname_service=>get_prosec_name( clsname =  ls_methodid-clsname ).
    ls_tn-obj_type = 'CPRO'.
    ls_tn-flag_check = ''.
    APPEND ls_tn TO et_tn.
  ENDMETHOD.                    "_object_for_meth
 METHOD _object_for_func.
    "к тому что осталось надо добавить инклуды TOP UXX и все инклуды не фм
    DATA: lt_tfdir TYPE TABLE OF tfdir
        , ls_tfdir  TYPE tfdir
        , lv_fugr(40)
        , lt_func TYPE TABLE OF char40
        , lv_func(40)
        , ls_tn   TYPE ty_s_tn
        .
    "Добавим сам фм
    ls_tn-obj_name = iv_obj_name.
    ls_tn-obj_type = 'FUNC'.
    ls_tn-flag_check = 'X'.
    APPEND ls_tn TO et_tn.


    "название фг
    SELECT SINGLE pname INTO lv_fugr FROM  tfdir
                       WHERE   funcname = iv_obj_name.

    SELECT * INTO TABLE lt_tfdir FROM tfdir
                 WHERE pname = lv_fugr.
lv_fugr = lv_fugr+3. "(SAPLZMMAA2 --> LZMMAA2 )

    "собрали все фм, которые инклуды и в инклуде *UXX
    LOOP AT lt_tfdir INTO ls_tfdir.
      CLEAR lv_func.
      lv_func = lv_fugr && 'U' && ls_tfdir-include.
      APPEND lv_func TO lt_func.
    ENDLOOP.
*           lv_obj_type    = iv_obj_type.
*           lv_object_name = iv_object_name.
    DATA: ls_environment_selection TYPE  envi_types VALUE 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'.
    DATA: lv_obj_type     TYPE euobj-id
        , lv_object_name  TYPE tadir-obj_name
        , lt_env TYPE  senvi_tab
        .

    lv_object_name = lv_fugr+1.
    lv_obj_type    = 'FUGR'.

    CALL FUNCTION 'REPOSITORY_ENVIRONMENT_SET_RFC'
      EXPORTING
        obj_type          = lv_obj_type
        environment_types = ls_environment_selection
        object_name       = lv_object_name  "идентификатор объекта (название)
      TABLES
        environment       = lt_env.
    "удаляем все, что не LZ*
    DELETE lt_env WHERE type <> 'INCL' AND object+0(2) <> 'LZ'.

    LOOP AT lt_func INTO lv_func.
      DELETE lt_env WHERE object = lv_func.
    ENDLOOP.

    DATA: ls_env LIKE LINE OF lt_env.
    LOOP AT lt_env INTO ls_env.
      ls_tn-obj_type = 'INCL'.
      ls_tn-obj_name = ls_env-object.
      ls_tn-flag_check = ''.
      APPEND ls_tn TO et_tn.
    ENDLOOP.

  ENDMETHOD.                    "_object_for_func

Собранные данные для метода и фм отправляем в 'REPOSITORY_ENVIRONMENT_SET_RFC'  

При этом получается, что мы собрали все методы и  области класса метода и все инклуды и фм группы функций фм. Делаем мы это для того, чтобы обезопасить перенос. Так как при переносе метода, фм, ГФ, класса - мы можем вызвать перегенерацию программ на переносимой среде и вызвать дампы при изменении сигнатуры или кода.  

_check_versiya_object_prod проверка версий объектов
 CALL FUNCTION 'SVRS_MASSCOMPARE_ACT_OBJECTS'
      EXPORTING
        it_e071          = it_e071                      "проверяемый объект
        iv_rfcdest_b     = mv_targ                       "целевая система
        iv_filter_lang   = ''
      IMPORTING
        et_compare_items = lt_compare_items          "таблица ответа
      EXCEPTIONS
        rfc_error        = 1
        not_supported    = 2
        OTHERS           = 3.
DELETE lt_compare_items WHERE equal <> ''.     " версии объектов = 
    DELETE lt_compare_items WHERE object = 'TABT'. "технические данные таблиц 

… отличия могут быть незначительные, такие как комментарий - их анализ будет проведен дополнительно 
LOOP AT it_e071 INTO ls_e071_fm.
      LOOP AT lt_compare_items INTO ls_items WHERE fragid  = ls_e071_fm-pgmid
                                              AND fragment = ls_e071_fm-object
                                              AND fragname = ls_e071_fm-obj_name.
 … добавляем сообщение в выходную таблицу  
ls_dat-comment  = 'Включите связ.объект в запрос. Разные версии'(003).
ls_dat-light    = '@0A@'. " красный
APPEND ls_dat TO mt_dat.

_loop_dat обработка собранных объектов
  1. дополнительно удалим, что собрали и что есть запросе

LOOP AT mt_dat ASSIGNING <ls_dat>.
      READ TABLE mt_e071 TRANSPORTING NO FIELDS
        WITH KEY object = <ls_dat>-object
                 obj_name = <ls_dat>-obj_name.
      IF sy-subrc = 0.
        CLEAR <ls_dat>.
      ENDIF.
    ENDLOOP.
  1. Ряд обработок по ГФ - если в запросе вся гф - то фм из нее не надо выводить - они перенесутся все.

  2. При проверках мы могли найти что версии разные, или есть изменения кода - которые отображает как отличия и ошибки, но на самом деле это или комментарии или код одинаков. Для этого проходим по ряду методов, которые сравнивают объекты на наличие реальных значимых изменений.

 LOOP AT mt_dat ASSIGNING <ls_dat> WHERE object = 'REPS'
                                         OR object = 'CPUB'
                                         OR object = 'CPRI'
                                         OR object = 'CPRO'.
      IF _check_real_dif_reps( is_dat =  <ls_dat> ) = ''.
        CLEAR <ls_dat>.
      ENDIF.
    ENDLOOP.
“ по факту мы программно запускаем REMOTE сравнение  
 METHOD _check_real_dif_reps.
    DATA: lt_rspar      TYPE rsparams_tt,
        ls_rspar      TYPE rsparams.
    DATA: lt_tb TYPE TABLE OF char300.
    DATA: lt_result TYPE match_result_tab .

    CLEAR: lt_rspar.

    CLEAR ls_rspar.
    ls_rspar-selname = 'OBJNAME'.
    ls_rspar-kind    = 'P'.
    ls_rspar-sign    = 'I'.
    ls_rspar-option  = 'EQ'.
    ls_rspar-low     = is_dat-obj_name.
    APPEND ls_rspar TO lt_rspar.

    ls_rspar-selname = 'OBJNAM2'.
    APPEND ls_rspar TO lt_rspar.

    CLEAR ls_rspar.
    ls_rspar-selname = 'OBJTYP1'.
    ls_rspar-kind    = 'P'.
    ls_rspar-sign    = 'I'.
ls_rspar-option  = 'EQ'.
    ls_rspar-low     = is_dat-object.
    APPEND ls_rspar TO lt_rspar.
    ls_rspar-selname = 'OBJTYP2'.
    APPEND ls_rspar TO lt_rspar.


    CLEAR ls_rspar.
    ls_rspar-selname = 'LOG_DEST'.
    ls_rspar-kind    = 'P'.
    ls_rspar-sign    = 'I'.
    ls_rspar-option  = 'EQ'.
    ls_rspar-low     = 'АДРЕС ПРОДУКТИВА'.
    APPEND ls_rspar TO lt_rspar.

    CLEAR ls_rspar.
    ls_rspar-selname = 'REM_SYST'.
    ls_rspar-kind    = 'P'.
    ls_rspar-sign    = 'I'.
    ls_rspar-option  = 'EQ'.
    ls_rspar-low     = 'BP1'.
    APPEND ls_rspar TO lt_rspar.

    SUBMIT  rsvrsrs3 WITH SELECTION-TABLE lt_rspar EXPORTING LIST TO MEMORY   AND RETURN  .

    DATA: lt_listobject TYPE TABLE OF  abaplist.
    CLEAR : lt_listobject
          , lt_result
          .
    CALL FUNCTION 'LIST_FROM_MEMORY'
      TABLES
        listobject = lt_listobject
      EXCEPTIONS
        not_found  = 1
        OTHERS     = 2.
    CALL FUNCTION 'LIST_TO_ASCI'
      TABLES
        listasci   = lt_tb
        listobject = lt_listobject
      EXCEPTIONS
        empty_list = 1.

    FIND ALL OCCURRENCES OF
     'Различий в исходных текстах этих версий нет.'
      IN TABLE lt_tb
      RESULTS lt_result IN CHARACTER MODE.
    IF sy-subrc = 0.
      rv_ok = ''.
      RETURN.
    ELSE.
      rv_ok = 'X'.
    ENDIF.

    IF rv_ok = 'X'.
*      исходные тексты обеих версий идентичны
      FIND ALL OCCURRENCES OF
    'исходные тексты обеих версий идентичны'
     IN TABLE lt_tb
     RESULTS lt_result IN CHARACTER MODE.
      IF sy-subrc = 0.
        rv_ok = ''.
      ELSE.
        rv_ok = 'X'.
      ENDIF.
    ENDIF.
 ENDMETHOD. 
и домены также
    LOOP AT mt_dat ASSIGNING <ls_dat> WHERE object = 'DOMA'
                                         OR object = 'DOMD'
                                         OR object = 'DTEL'
                                        OR object  = 'DTED'.
      IF _check_real_dif_d( is_dat =  <ls_dat> ) = ''.
        CLEAR <ls_dat>.
      ENDIF.
    ENDLOOP.
“по доменам делаем как выше только сабмитим другой отчет 
  SUBMIT radvvde2 WITH SELECTION-TABLE lt_rspar EXPORTING LIST TO MEMORY AND RETURN.

“Для таблиц   
  LOOP AT mt_dat ASSIGNING <ls_dat> WHERE object = 'TABL'
                                         OR object = 'TABD'.
      IF _check_real_dif( is_dat =  <ls_dat> ) = ''.
        CLEAR <ls_dat>.
      ENDIF.
    ENDLOOP.

    SUBMIT  radvvtb2 WITH SELECTION-TABLE lt_rspar EXPORTING LIST       TO MEMORY   AND RETURN  .

_find_not_transport_request Проверка не перенесенных запросов по другим задачам
METHOD _find_not_transport_request.
    DATA: lt_e071 TYPE TABLE OF e071.
    DATA: BEGIN OF ls_tab
        , trkorr TYPE e071-trkorr
        , as4date TYPE e070-as4date
        , as4time TYPE e070-as4time
        , as4text TYPE e07t-as4text
        , END OF ls_tab
        , lt_tab LIKE TABLE OF ls_tab
        .
    DATA: lt_task TYPE ty_t_task
        , ls_task TYPE ty_s_task
        .
    DATA: ls_methodid     TYPE seocpdkey.
    DATA: lt_split        TYPE TABLE OF sobj_name
        , ls_split        TYPE sobj_name
        .

    SELECT * INTO CORRESPONDING FIELDS OF TABLE lt_tab
      FROM e071 INNER JOIN e070 ON e071~trkorr = e070~trkorr
                INNER JOIN e07t ON e07t~trkorr = e070~trkorr

      WHERE e071~obj_name = iv_object_name    AND e070~trfunction = 'K' "инстр.стр/ T - перенос копий/ S - разработка корректура  и т.д.
        .

    "расширяем запросы для FUNC - нужны запросы и с FUGR  его
    "ищем ГФ для FUNC
    IF iv_object_type = 'FUNC'.
      "название фг
      DATA: lv_fugr TYPE pname.
      SELECT SINGLE pname INTO lv_fugr FROM  tfdir
                      WHERE   funcname = iv_object_name.
 lv_fugr = lv_fugr+4. "(SAPLZMMAA2 --> LZMMAA2 )

      SELECT * APPENDING CORRESPONDING FIELDS OF TABLE lt_tab
      FROM e071 INNER JOIN e070 ON e071~trkorr = e070~trkorr
                INNER JOIN e07t ON e07t~trkorr = e070~trkorr
      WHERE e071~obj_name =  lv_fugr
        AND e071~object   = 'FUGR'
        AND e070~trfunction = 'K'  .
    ENDIF.

    IF iv_object_type = 'METH'.
      CLEAR lt_split.
      CLEAR ls_split.

      SPLIT iv_object_name AT space INTO TABLE lt_split.
      DELETE lt_split WHERE table_line IS INITIAL.

      READ TABLE lt_split INTO ls_methodid-clsname  INDEX 1.
      READ TABLE lt_split INTO ls_methodid-cpdname  INDEX 2.

      SELECT * APPENDING CORRESPONDING FIELDS OF TABLE lt_tab
      FROM e071 INNER JOIN e070 ON e071~trkorr = e070~trkorr
                INNER JOIN e07t ON e07t~trkorr = e070~trkorr
      WHERE e071~obj_name =  ls_methodid-clsname
        AND e071~object = 'CLAS'
        AND e070~trfunction = 'K'    .
    ENDIF.


    SORT lt_tab BY as4date DESCENDING  "дата деблокирования ( по ней сортировка           
                                                         при просм версионности
                   as4time DESCENDING .

    DATA: lv_i TYPE i.
    DATA: lv_err TYPE flag.
    READ TABLE lt_tab INTO ls_tab WITH KEY trkorr = mv_trkorr.
    lv_i = sy-tabix + 1.

    LOOP AT lt_tab INTO ls_tab FROM lv_i. "с запроса нашего - ищем запросы ДО нашего ( ниже )
      CLEAR lt_task.
      _pars_task( EXPORTING iv_as4text = ls_tab-as4text
                  IMPORTING et_task    = lt_task   ).
      IF mt_task = lt_task.
        CONTINUE. "по нашей задаче нас не интересует - все должно быть тут
      ENDIF.
      CLEAR lv_err.
      LOOP AT lt_task INTO ls_task.
        READ TABLE mt_task TRANSPORTING NO FIELDS
                            WITH KEY proj = ls_task-proj
                                    nomer = ls_task-nomer.
        IF sy-subrc <>  0.
          lv_err = 'X'.
          EXIT. "выходим
        ENDIF.
      ENDLOOP.
      IF sy-subrc = 0.
        IF lv_err = ''.
          CONTINUE.
        ENDIF.
      ENDIF.
      DATA: ls_tr_text TYPE ty_s_tr_text.
      "а вот если нет, начинаем проверять - донесен ли запрос
      IF _check_transport( iv_trkorr = ls_tab-trkorr ) = 'X'.
        ls_tr_text-trkorr = ls_tab-trkorr.
        COLLECT ls_tr_text INTO et_tr_text.
      ELSE.
 EXIT. 
      ENDIF.
    ENDLOOP.

  ENDMETHOD.


METHOD _pars_task.
    DATA:
      lv_text TYPE as4text,
      lv_i TYPE i,
      lv_proj(10),
      lv_nomer(10),
      ls_task TYPE ty_s_task,
      lv_as4text TYPE as4text.

    lv_as4text = iv_as4text.
    CONDENSE lv_as4text NO-GAPS.

    DO. 
      IF lv_as4text = ''.
        EXIT.
      ENDIF.
      IF lv_as4text+0(1) CN '0123456789-,;' . "не число и не дефис
        lv_proj = lv_proj && lv_as4text+0(1).
      ENDIF.

      IF lv_as4text+0(1) = '-'.  "пропускаем
        lv_as4text = lv_as4text+1.
        CONTINUE.
      ENDIF.

      IF lv_as4text+0(1) CO '0123456789' . "число
        lv_nomer = lv_nomer && lv_as4text+0(1).
        IF lv_as4text+1(1) CN '0123456789,;' .. "не число
          ls_task-nomer = lv_nomer.
          ls_task-proj = lv_proj.
          APPEND ls_task TO et_task.
          CLEAR lv_nomer.
          CLEAR lv_proj.
          EXIT.  "конец
        ENDIF.
      ENDIF.

      IF lv_as4text+0(1) = ',' OR lv_as4text+0(1) = ';' .
        ls_task-nomer = lv_nomer.
        ls_task-proj = lv_proj.
        APPEND ls_task TO et_task.
        IF lv_as4text+0(1) = ','.
          CLEAR lv_nomer.
        ENDIF.
        IF lv_as4text+0(1) = ';'.
          CLEAR lv_nomer.
          CLEAR lv_proj.
        ENDIF.
      ENDIF.
      lv_as4text = lv_as4text+1.

    ENDDO.
    SORT et_task.

  ENDMETHOD.                    "_pars_task


  METHOD _check_transport.
    DATA: lt_rspar      TYPE rsparams_tt,
          ls_rspar      TYPE rsparams.

    DATA: lt_tb TYPE TABLE OF char300.
    DATA: ls_tb TYPE char300.
    DATA: lt_result TYPE match_result_tab .
    CLEAR: lt_rspar.

    CLEAR ls_rspar.
    ls_rspar-selname = 'PV_KORR'.
    ls_rspar-kind    = 'P'.
    ls_rspar-sign    = 'I'.
    ls_rspar-option  = 'EQ'.
    ls_rspar-low     = iv_trkorr.
    APPEND ls_rspar TO lt_rspar.

    SUBMIT  rddprott WITH SELECTION-TABLE lt_rspar EXPORTING LIST TO MEMORY   AND RETURN  .

    DATA: lt_listobject TYPE TABLE OF  abaplist.
    CLEAR : lt_listobject
          , lt_result
          .
    CALL FUNCTION 'LIST_FROM_MEMORY'
      TABLES
        listobject = lt_listobject
      EXCEPTIONS
        not_found  = 1
        OTHERS     = 2.
    CALL FUNCTION 'LIST_TO_ASCI'
      TABLES
        listasci   = lt_tb
        listobject = lt_listobject
      EXCEPTIONS
        empty_list = 1.

    DATA: lv_i TYPE i.
    rv_err = 'X'.
    READ TABLE lt_tb INTO ls_tb WITH KEY table_line = '      ---   BP1        System BP1'.
    IF sy-subrc <> 0.
      READ TABLE lt_tb INTO ls_tb WITH KEY table_line = '      |--   BP1        System BP1'.
      IF sy-subrc <> 0.
        RETURN.
      ELSE.
        lv_i = sy-tabix.
        rv_err = 'X'.
      ENDIF.
    ELSE.
      lv_i = sy-tabix.
      rv_err = 'X'.
    ENDIF.
    LOOP AT lt_tb INTO ls_tb FROM lv_i.
      IF  ls_tb CS '-----       Импорт' AND ( ls_tb CS  'Успешно завершено'
                                                      OR ls_tb CS  'Заверш. с ошибкой'
                                                      OR ls_tb CS  'Заверш. с предупр.' ). "ошибка - тоже перенос
        rv_err = ''.
        RETURN.
      ENDIF.
    ENDLOOP.

  ENDMETHOD.                    "_check_transport

 CALL FUNCTION 'TMS_UIQ_IQD_READ_QUEUE'   - считывание очереди переносов на продуктиве через DESTINATION

Проверка нейминга

Для анализа написанного кода в ABAP существует транзакция SCII

В ней можно проверять как целый запрос, так и отдельные объекты. SCII анализирует объекты полностью, перед нами же стоит задача настроить анализатор на дельту кода между системой разработки и продуктивом. Также необходимо оставить возможность настройки через стандартный интерфейс SCII. Изначально был реализован функциональный модуль, который возвращает нам результаты проверки через SCII конкретного запроса

Hidden text
CALL FUNCTION 'ZBS_FM_INSPECT_LIST'
	EXPORTING
  	iv_sci_name         	= iv_sci_name - имя предварительно настроенного варианта из SCII
  	iv_sci_user         	= iv_sci_user - имя пользователя
  	iv_korr                 = iv_trkorr - номер запроса
	IMPORTING
  	et_results      	    = lt_scit_rest_buf
  	et_results_hd       	= lt_scit_resh
  	et_variant          	= lt_variant
	EXCEPTIONS
  	no_object           	= 1
  	objs_already_exists 	= 2
  	no_default_checkvariant = 3
  	too_many_objects    	= 4
  	could_not_read_variant  = 5
  	OTHERS              	= 6.

В результате мы получаем полную информацию о месте в коде, которое не прошло предварительно настроенную проверку:

Здесь:

  • SOBJTYPE — тип объекта;

  • SOBJNAME — название объекта;

  • LINE и COL — строка и столбец в тексте программы;

  • PARAM1 — указание на название переменной.

Функциональный модуль был написан через механизм работы транзакции SCII

Hidden text
DATA:
	lo_objs_ref TYPE REF TO cl_ci_objectset,
	lo_chkv_ref TYPE REF TO cl_ci_checkvariant,
	lo_insp_ref TYPE REF TO cl_ci_inspection.
 
  cl_ci_objectset=>get_ref(
	EXPORTING
  	p_type           	= iv_type - 0KOR
  	p_korr           	= iv_korr
  	RECEIVING
  	p_ref            	= lo_objs_ref
	EXCEPTIONS
  	invalid_request       	= 1
  	object_may_not_be_checked = 2
  	OTHERS                	= 3 ).
 
  IF sy-subrc = 1.
	RAISE objs_already_exists.
  ENDIF.
 
  "get local check variant
  cl_ci_checkvariant=>get_ref(
	EXPORTING
  	p_user        	= iv_sci_user
  	p_name        	= iv_sci_name
	RECEIVING
  	p_ref         	= lo_chkv_ref
	EXCEPTIONS
  	chkv_not_exists   = 1
  	missing_parameter = 2
  	OTHERS        	= 3 ).
 
  IF lo_chkv_ref IS INITIAL.
	RAISE could_not_read_variant.
  ENDIF.
 
  lo_chkv_ref->get_info(
	EXCEPTIONS
  	could_not_read_variant = 1
  	OTHERS             	= 2 ).
 
  IF sy-subrc = 1.
	RAISE could_not_read_variant.
  ENDIF.
 
  et_variant = lo_chkv_ref->variant.
 
  cl_ci_inspection=>create(
   	EXPORTING
     	p_user          	= iv_sci_user
     	p_name          	= space
   	RECEIVING
     	p_ref           	= lo_insp_ref
   	EXCEPTIONS
     	insp_already_exists = 1
     	insp_not_exists 	= 2
     	OTHERS          	= 3 ).
 
  IF sy-subrc <> 0.
	RETURN.
  ENDIF.
 
  lo_insp_ref->set(
	EXPORTING
  	p_chkv = lo_chkv_ref
  	p_objs = lo_objs_ref ).
 
  CALL METHOD lo_insp_ref->run
	EXPORTING
  	p_howtorun         	= 'D'
	EXCEPTIONS
  	missing_information	= 1
  	cancel_popup       	= 2
  	insp_already_run   	= 3
  	no_object          	= 4
  	too_many_objects   	= 5
  	could_not_read_variant = 6
  	locked             	= 7
  	objs_locked    	    = 8
  	error_in_objs_build	= 9
  	invalid_check_version  = 10
  	OTHERS             	= 11.
 
  CASE sy-subrc.
	WHEN 4.
  	RAISE no_object.
	WHEN 5.
  	RAISE too_many_objects.
	WHEN OTHERS.
  	"
  ENDCASE.
 
  et_results	= lo_insp_ref->scirestps. - на выходе получаем нужные нам данные со скриншота выше
  et_results_hd = lo_insp_ref->sciresthd.

Теперь необходимо соотнести результаты полной проверки объектов из запроса с разницей версий между системой разработки и продуктивом. Для этого необходимо использовать механизм через который работает транзакция для сравнения версий — SE39. Изучив, как работает транзакция в дебаггере, можно заметить, что она использует следующие функциональные модули:

Hidden text
CALL FUNCTION 'SVRS_GET_REPS_FROM_OBJECT'
	EXPORTING
  	destination              	= lv_logdest_lft - адрес системы разработок
  	object_name                  = lv_objname_lft - название программы для анализа
  	object_type                  = lv_objtype_lft - тип объекта, скорей всего PROG
  	versno                   	= lv_version_lft - можно ничего не передавать, будет анализироваться текущая версия
  	iv_no_release_transformation = abap_true
	TABLES
  	repos_tab                    = lt_abaptext_sec - текст программы из системы разработок
  	trdir_tab                	= lt_trdir_sec
  	vsmodilog                	= lt_smodilog_lft
	EXCEPTIONS
  	no_version               	= 01.
 
  CALL FUNCTION 'SVRS_GET_REPS_FROM_OBJECT'
	EXPORTING
  	destination              	= lv_logdest_rgt - адрес продуктивной системы
  	object_name                  = lv_objname_rgt - название объекта для анализа
  	object_type                  = lv_objtype_rgt - тип объекта, скорей всего PROG
  	versno                   	= lv_version_rgt - можно ничего не передавать, будет анализироваться текущая версия
  	iv_no_release_transformation = ''
	TABLES
  	repos_tab                    = lt_abaptext_pri - текст программы из продуктивной системы
  	trdir_tab                	= lt_trdir_pri
  	vsmodilog                	= lt_smodilog_rgt
	EXCEPTIONS
  	no_version               	= 01.
 
  CALL FUNCTION 'SVRS_COMPUTE_DELTA_REPS'
	EXPORTING
  	compare_mode        	= lv_comp_mode
  	ignore_case_differences = lv_ignore_case_differences
	TABLES
  	texttab_old         	= lt_abaptext_sec
  	texttab_new         	= lt_abaptext_pri
  	trdirtab_old        	= lt_trdir_sec
  	trdirtab_new        	= lt_trdir_pri
  	trdir_delta         	= lt_trdir_delta
  	text_delta          	= lt_abaptext_delta. - получаем нужную нам для анализа дельту

Далее получаем таблицу изменений lt_linbuffer_dev. Замечу, что rp_write_first_block, rp_write_last_block, rp_write_next_block были скопированы и адаптированы из SE39

Hidden text
CLEAR: gt_abaptext_delta[],
     	gt_abaptext_sec[],
     	gt_abaptext_pri[],
     	gt_linbuffer[].
 
  APPEND LINES OF lt_abaptext_delta TO gt_abaptext_delta[].
  APPEND LINES OF lt_abaptext_sec TO gt_abaptext_sec[].
  APPEND LINES OF lt_abaptext_pri TO gt_abaptext_pri[].
 
  """""""""""""""""""""""""""
  "Копия со стандарта
  "Получаем таблицу сравнения версий - gt_linbuffer
  DESCRIBE TABLE gt_abaptext_delta LINES gv_dltlines.
 
  IF lines( gt_abaptext_pri ) > lines( gt_abaptext_sec ).
	gv_txtlines = lines( gt_abaptext_pri ).
  ELSE.
	gv_txtlines = lines( gt_abaptext_sec ).
  ENDIF.
 
  PERFORM rp_write_first_block USING lv_offs_hscroll.
  DO.
	IF gv_delta_ind > gv_dltlines.
  	PERFORM rp_write_last_block USING lv_offs_hscroll.
  	EXIT.
	ENDIF.
	PERFORM rp_write_next_block USING lv_offs_hscroll.
  ENDDO.
  """""""""""""""""""""""""""
 
  lt_linbuffer_dev[] = gt_linbuffer.

Изменения кода теперь лежат в таблице lt_linbuffer_dev. Остается соотнести содержимое таблиц lt_scit_rest_buf и lt_linbuffer_dev, чтобы получить результаты проверки SCII только по изменениям, внесенным в код в рамках текущего запроса. Соотносим их по номеру строки, после чего задачу можно считать выполненной.

Хардкод

Помимо проверки кода через стандартный анализатор, стояла задача избавиться от прямого указания конкретных параметров в коде. Изначально у нас уже есть номер запроса, поэтому получаем из него список объектов. Далее по каждому объекту получаем информацию из репозитория

Hidden text
SELECT SINGLE *
FROM tadir
	INTO CORRESPONDING FIELDS OF TABLE ls_stadir
	WHERE object   = lv_objtype
  	AND obj_name = lv_objname.

Для работы с параметрами, от которых нужно избавиться, используется настроечная таблица. Из этой таблицы мы считываем значения и передаем их в lv_hardvalue. Подготавливаем таблицу lt_rspar для анализа объекта на вхождение значения CSVALUE:

Hidden text
CLEAR ls_rspar.
	ls_rspar-selname = 'S_DEVC'.
	ls_rspar-kind	= 'S'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= ls_stadir-devclass.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'S_REST'.
	ls_rspar-kind	= 'S'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= ls_stadir-obj_name.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'P_EXCOMM'.
	ls_rspar-kind	= 'P'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= 'X'.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'P_STRG1'.
	ls_rspar-kind	= 'P'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= lv_hardvalue.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'P_PROG'.
	ls_rspar-kind	= 'P'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= 'X'.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'P_FUGR'.
	ls_rspar-kind	= 'P'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= 'X'.
	APPEND ls_rspar TO lt_rspar.
 
	CLEAR ls_rspar.
	ls_rspar-selname = 'P_CINC'.
	ls_rspar-kind	= 'P'.
	ls_rspar-sign	= 'I'.
	ls_rspar-option  = 'EQ'.
	ls_rspar-low 	= 'X'.
	APPEND ls_rspar TO lt_rspar.

Далее получаем результат анализа таким образом:

Hidden text
DATA: lref_data TYPE REF TO data.
	cl_salv_bs_runtime_info=>set( display    	= abap_false
                              	metadata   	= abap_false
                              	data       	= abap_true ).
	SUBMIT afx_code_scanner WITH SELECTION-TABLE lt_rspar AND RETURN.
	CLEAR lt_tab_lines[].
 
	TRY.
    	cl_salv_bs_runtime_info=>get_data( IMPORTING t_data = lt_tab_lines[] ).
  	CATCH cx_salv_bs_sc_runtime_info.
    	RETURN.
	ENDTRY.
    cl_salv_bs_runtime_info=>clear_all( ).

Так мы получаем в lt_tab_lines нужную нам информацию.

Что дальше

Автоматизация проверки запросов уже позволила нам получить превосходный результат в плане улучшения качества кода. Мы смогли значительно снизить ошибки при переносе и сэкономить время, которое тратилось на проверку “руками”. Но нет предела совершенству, и мы уже ведем работы по расширению возможностей программы.

В перспективе планируется анализировать на хардкод только текущие изменения в коде с помощью механизма получения разницы версий, описанного в разделе Проверка нейминга. Сейчас разрабатываются функционал проверки на хардкод в дельте кода, идут работы с оптимизацией, проверки на оригинал системы, на использование объектов во время переноса и ряд других функций.

Комментарии (0)