«Чтобы сразу как надо, не переделывая». Зачем мы учили дизайнеров проводить исследования, и к чему это привело

В некоторых компаниях исследовательская лаборатория — это примерно как исследовательское агентство, только расположенное не снаружи, а внутри. В других компаниях исследователи связаны с проектными командами больше, чем с коллегами-ресерчерами. А у кого-то вообще нет формально выделенных исследователей, эту функцию может брать на себя UX-дизайнер, когда возникает такая необходимость. Мы в «Лаборатории Касперского» — я, UX Research Group Manager Лена Королева, и мой соавтор Senior Usability Researcher Даша Романова — попробовали жить в разных конфигурациях и можем рассказать:

  • в чем состоят плюсы и минусы каждого варианта;
  • какие подводные камни ждут вас при смене модели;
  • что делать, чтобы с минимальными потерями вырулить к новой «точке стабильности».

image

Исходное состояние UX-ресерча: «Лаборатория в “Лаборатории”»

До ноября 2019 года мы жили в парадигме «Внутренняя юзабилити-лаборатория». В какой-то момент времени, чаще всего приуроченный к релизу нового функционала или готовности макетов для него, к нам приходил продакт-менеджер и рассказывал, что он хочет узнать. В большинстве случаев его интересовало качество интерфейса, реже — общие впечатления от продолжительного использования продукта, еще реже — потребности, контекст и модель использования. На проект назначался исследователь, который принимал на себя все задачи по организации, проведению, обработке и презентации результатов. Участие заказчика и дизайнера зависело только от их доброй воли и могло колебаться в широких пределах. Бывало и так, что мы видели их в первый раз на установочной встрече, а во второй и последний — на презентации. Отчеты отправлялись в централизованное хранилище вместе с программой исследования и видеозаписями.

Можно довольно уверенно сказать, что система работала как часы — 100% желающих получали свои результаты в оговоренные и предсказуемые сроки, надлежащего качества и в рамках имеющегося бюджета.

Звучит неплохо, правда? В общем, да, но есть нюанс. Приблизительно 70% обнаруженных проблем не исправлялись. И самое печальное — не исправлялись не только на уровне разработки, но и на уровне макетов. Никакой злой воли, просто как-то руки не доходили. Истинная цель исследований состояла не в том, чтобы сделать пользовательский опыт прекрасным и незабываемым. Задачей-минимум (она же максимум) было исправить косяки, делающие использование продукта невозможным или трудновыносимым. Да, есть вещи поважнее UX. Например, time to market и стоимость разработки — позиция совершенно понятная и даже легитимная, но ужасно несовременная!

Как обосновывались изменения: «Помешиваем супчик»

Наши топ-менеджеры любят и при всяком удобном случае используют эту метафору: «Чтобы супчик не прокис, его надо помешивать». Новый амбициозный и энергичный руководитель UX-дизайна взялся за большую ложку.

Отныне дизайнерам надлежало проводить исследования самостоятельно. Знание о пользователях, бывшее внешним и формальным, должно было стать внутренним и личным. Мысль совершенно разумная: человек, однажды видевший, как после установки тестовой сборки продукта у респондента перестал работать адаптер Wi-Fi, никогда не будет прежним. Даже если он не метался в поисках компьютерного мастера в Чебоксарах, который переустановит Windows и сделает все как было (а у нас на удаленных тестах как-то был и такой случай), впечатлений ему хватит надолго. Раз за разом погружаясь в поток пользовательских переживаний, дизайнер обретает глубокий, проработанный образ пользователя и его деятельности. И теперь он уже не может делать вид, что проблем не существует: товарищ Совесть настигнет его через недели и месяцы после проведения тестов.

Для исследователей диспозиция тоже менялась. Вместо того чтобы проводить «рутинные и типовые» тесты, дневниковые исследования, опросы и интервью, они смогут посвятить себя сложным, необычным, требующим особых навыков проектам (каковые пока на горизонте не появлялись, но вскоре обязательно должны были появиться).

Звучит хорошо? Звучит отлично! Осталось взять и сделать.

image

Мотивация: «Доброе слово и пистолет. Нет, “доброе слово” здесь лишнее»

Свободная воля дана человеку для того, чтобы он мог уклониться от блага. Дизайнеры восприняли новость без особого восторга.

— Но мы этого не умеем!
— Ничего, научитесь. Глаза боятся, а руки делают.
— Но мы и так перегружены рисованием макетов!
— Это не страшно. После того как вы поймете боли пользователей, вы сразу будете рисовать как надо, а не переделывать по сто раз.
— Но ведь мы пришли работать дизайнерами, а не исследователями! У нас так даже в должностной инструкции написано!
— Что за формализм! Где ваши горящие глаза?! А гибкость где?!
— Когда надо начать?
— Прямо сейчас. Встаньте и идите. Не благодарите.

Исследователи тоже не могли отделаться от тревожных мыслей. «Если буквально любой может проводить исследования без всякой подготовки, на что я потратил пять лет в институте и еще от 5 до 15 в исследовательском отделе?». А также: «Чем буду заниматься я, когда все исследования станут проводить дизайнеры? Может быть, и мне пора освоить пару-тройку смежных специальностей?» (Нет, «сложные необычные проекты» на тот момент всерьез не рассматривались.) Ну и в целом: «Капец, у меня забирают мою работу».

Что делать?

Постарайтесь поскорее проскочить стадии отрицания и гнева и приступить к торгу. После того как встревоженные дизайнеры начали массово назначать нам получасовые встречи с названием «Как проводить исследования», стало ясно, что нужны какие-то системные решения. Мы предложили к каждому дизайнеру, проводящему исследование, приставить ресерчера, который будет его обучать, направлять и ободрять. Была надежда, что таким образом мы избежим резкого проседания по срокам и качеству исследований и сохраним доверие со стороны коллег из других подразделений. Надежда оправдалась: дизайнеры почувствовали себя спокойнее, коллеги-смежники, кажется, вообще не заметили, чтобы в ресерче что-то поменялось.

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

  • Во-первых, не все успели провести те самые 2-3 исследования, особенно часто такое случается с В2В-продуктами.
  • Во-вторых, даже при самых удачных обстоятельствах исследования сильно разнесены во времени, и у дизайнеров не успевают сложиться и закрепиться навыки.
  • В-третьих, дизайнеры по разным причинам не смогли взять на себя организацию исследования (рекрутинг респондентов, сценарий тестирования, расписание тестовых сессий), обработку данных и подготовку отчета. Фактически это означает, что ресерчер делает все то же, что обычно, за исключением модерации.
  • И в-четвертых, состав дизайнеров меняется, приходят новые люди, за которыми тоже нужно присматривать. (Нет, на рынке труда не удалось найти людей, которые умеют исследовать так же хорошо, как дизайнить, by default.)

image

Время: «Я проснулся среди ночи и понял, что все идет по плану. Все идет по плану»

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

Что делать?

  • Любой эффективный менеджер знает ответ: планировать! Смысл в том, чтобы все задачи — и по дизайну, и по исследованиям — попали в единую очередь, с проставленными сроками и приоритетами. У нас в «Лаборатории Касперского» управление задачами разработки происходит через TFS. Туда же были заведены задачи дизайнеров, с описанием скоупа и целевыми датами, которые могут меняться и двигаться при возникновении новых вводных. Отныне стало нельзя просто так прибежать к дизайнеру с просьбой «нарисовать кнопочку по-быстрому», все регламентируется и выстраивается в очередь — ту же самую, где стоят запланированные исследования.
  • Смягчить требования по срокам проведения полевых работ. Вместо 3-4 сессий в день мы стали назначать 1-2. Но даже в этом случае найти 2-3 часа на тесты и еще 2 на рекапы дизайнерам довольно сложно. Подстроиться под желания респондентов — практически невозможно.
  • Перестать надеяться на то, что дизайнеры будут в состоянии уследить за расписанием тестов и переносами. Все встречи назначаются через календарь, также закладываются временные слоты для замен и переносов.

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

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

image

Знание теории и практики: «Грести научат на галерах»

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

Что делать с теоретической подготовкой?

  • Подготовить список литературы для чтения. Лучше начать с чего-то несложного и понятно написанного, типа «Спросите вашу маму». Книги Белановского отложите до лучших времен.
  • Организовать обучение. Нам хорошо: одна из множества опций в списке материальных и нематериальных бенефитов «Лаборатории» — создание полноценного learning journey, когда каждый может выстроить оптимальный для себя план развития. Если у вас ничего подобного нет, проведите хотя бы серию митингов. Можно пересказывать и комментировать уже готовые курсы типа Nielsen Norman Group. Если этого недостаточно, придется написать собственные материалы по техникам модерирования и выявления UX-проблем.
  • Посоветовать посмотреть записи уже проведенных тестов и интервью (у нас большой и содержащийся в образцовом порядке архив лет примерно за десять, там почти наверняка есть что-то по интересующему дизайнера продукту или направлению).
  • Написать пошаговый гайд по проведению исследований. С примерами, шаблонами рабочих документов и понятными картинками.

Что делать с практическими навыками?

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

image

Проблемы, для которых мы пока не придумали хорошего решения

Знание предметной области. В первую очередь речь идет о знаниях в области информационной безопасности. Даже если вся информация лежит на блюде (нет, не лежит, в лучшем случае там лежат толстые книжки на английском), для ее осознания нужно время, желательно рабочее, а чтобы его находить, нужна мотивация, и зачастую не только самого дизайнера, но и всей команды продукта — дедлайны все-таки дело общее.

Один из наших В2В-дизайнеров сделал нетиповой ход — попытался устроиться стажером в отдел нашей же секьюрити-аналитики, чтобы таким образом проникнуть в секреты их мастерства и познать насущные проблемы потенциальных юзеров без отрыва от производства. Его попытки отвлечь от работы очень занятых и чрезвычайно высокооплачиваемых людей были мягко, но решительно отвергнуты.

Research Ops. Дизайнеры мало-помалу понимают, почему они должны заниматься чем-то высоким и приближенным к продукту, то есть самим проведением тестов/интервью, но организация, составление расписания, работа с агентствами, постоянный контроль процесса, отнимающие ооочень много времени («Вау, я и не знал, что у ресерчера столько дел!») — это же совсем другое. Это не дает ни знаний о продукте, ни новых инсайтов, да и просто сложно в конце концов.

Пока мы сошлись на том, что эту работу делают ресерчеры. Хорошо ли это для самих ресерчеров — сложный вопрос. С одной стороны, ресерчер теперь «менеджер» процесса исследования, и у него есть больше ресурсов для работы с инсайтами (объективно модерация очень сжирает эти самые ресурсы). С другой — «обслуживание» по-прежнему занимает сопоставимую часть рабочего времени, то есть, по сути, одну работу разбили на двоих. Но да, формально с утверждением «дизайнеры в ”Лаборатории Касперского” сами проводят исследования» не поспоришь.

Иностранные респонденты. У нас международные продукты. То есть ими пользуются не только в России. Не только русские люди, ага. То есть тесты и интервью нужно проводить еще и хотя бы на английском языке. Учитывая при этом, что респонденты находятся в других часовых поясах.

Не так давно у нас появилась программа обучения английскому, созданная специально для ресерчеров и дизайнеров. Успехи есть. Когда мы все сможем вести тесты ночью и свободно поддерживать разговор о выявлении аномалий в данных телеметрии — вопрос открытый.

image

Итоги. Lessons learned

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

  • «Все, что мое — твое. Возьми, у меня еще есть». Проблемы, описанные в отчете, перестают быть просто словами. За каждой стоят живые реальные люди, их опыт борьбы, страдания и победы (если повезет). Дизайнер приобретает то, что называется ownership — свою долю сопричастности и ответственности за происходящее с клиентами.
  • «Демократизируй это». Когда у вас уже есть готовые инструменты и правила для совместного проведения исследований, грешно ограничиваться одними дизайнерами. Двери юзабилити-лаборатории естественным образом открываются для смежных отделов. В нашем случае это менеджеры продуктов, технические писатели, маркетинг и разработка. Исследовательская практика становится более демократичной, формируется более объемный и сбалансированный (и при этом общий) взгляд на продукт и его будущее. После пережитого опыта все эти люди, работающие над одним продуктом, становятся настоящей командой!
  • «Часть корабля — часть команды». Со стороны ресерча происходит встречное движение: появляется возможность не только диагностировать проблемы, но и участвовать в выработке решений и — НАКОНЕЦ-ТО! — отслеживать внедрение результатов и планирование дальнейших итераций.
  • «Наращиваем не вес, а мышечную массу». Объем собираемых данных почти не меняется, но сильно улучшается их доступность и «усвояемость».

При этом нелишним будет и помнить о цене, которую придется за это заплатить.

  • Работы у ресерчеров не станет меньше, будет ровно наоборот. Они будут делать все то же, что и до, плюс обучение, контроль, организация процессов участия и обсуждения. Внутренней коммуникации станет резко больше, и она займет до 50% общего бюджета рабочего времени.
  • Сами исследовательские проекты будут длиться дольше, но их количество не изменится, просто они будут накладываться друг на друга по времени. Это означает частые переключения между проектами и командами, что само по себе увеличивает нагрузку.
  • Нагрузка дизайнеров тоже вырастет — и почасовая, и ментальная. Нет, они не станут рисовать быстрее, но будут делать это более осознанно.
  • Если у вас международные продукты, всем придется доучить английский до уровня, позволяющего вести сессии или хотя бы понимать происходящее.

Теперь советы для ресерчеров:

  • Узбагойтесь. Рано или поздно (в нашем случае месяца за 3-4) ситуация как-то входит в нормальные берега. Возможно, вам даже понравится.
  • Не молчите. Изменение процессов — это скользкое шоссе, и вы несетесь по нему в обычном легковом автомобиле; так погибает огромное число людей ежедневно. Обсуждайте, изучайте чужой опыт, приводите аргументы, смотрите с разных сторон (в том числе со стороны бизнеса). Даже если изначально не согласны с выработанным наверху планом, не ограничивайтесь критикой, предложите что-нибудь разумное.
  • Заранее закладывайте в планы дизайнеров 16 часов чистого времени на подготовку и по 4 часа на каждую тестовую сессию.
  • Если от дизайнеров не будет требоваться подготовка отчета — скажите об этом, это сильно успокаивает. Вообще будьте готовы много раз обсуждать даже самые, на ваш взгляд, простые вещи.
  • Если вы все же хотите, чтобы дизайнеры писали отчеты, будьте готовы к тому, что они будут отрывочными, местами неполными и написанными сильно позже проведения встреч с респондентами (if ever). Ну или честно заложите на эту работу 40 часов чистого времени.
  • Не идите на поводу своего желания вообще отказаться от отчетов, «ведь все и так понятно». Понятно оно ровно в момент проведения сессии, после 5-6 сессий все сливается в какой-то невнятный туман. Помните: «Не записанного не существует».
  • Проводите ретроспективы. После каждого ресерча обсуждайте и сам процесс его проведения, проговаривайте ошибки и ценные находки, что было круто, а что можно было сделать по-другому, как лучше действовать в следующий раз.
  • Не бросайте дизайнеров с обрыва в бурлящую реку исследований одних, обучайте и будьте терпеливыми, давайте обратную связь и советуйте, как справляться с проблемами, через которые уже проходили не раз.

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

image


ссылка на оригинал статьи https://habr.com/ru/company/kaspersky/blog/653741/

Агрегация

По материалам статьи Craig Freedman: Aggregation

Агрегация относится к таким операциям, когда больший набор строк свёртывается в меньший. Типичные агрегатные функции — COUNT, MIN, MAX, SUM и AVG. SQL Server поддерживает также и другие агрегаты, типа STDEV и VAR.

Я собираюсь посвятить этой теме несколько статей. В этой статье, я сосредоточусь на «Скалярных Агрегатах». Скалярные агрегаты — запросы с агрегатными функциями в списке оператора SELECT и без предложения GROUP BY. Скалярные агрегаты всегда возвращают одну строку.

Скалярные агрегаты

Существует только один оператор скалярной агрегации: Stream Aggregate — статистическое выражение потока. Например:

create table   t (a int, b int,   c int) select count(*)   from t

|—Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1005],0)))

       |—Stream Aggregate(DEFINE:([Expr1005]=Count(*)))

            |—Table Scan(OBJECT:([t]))

Это своеобразный «Hello World!» в агрегации, статистическое выражение потока только и делает, что считает число строк на входе и возвращает результат подсчёта. Фактически, статистическое выражение потока вычисляет count ([Expr1005]) как bigint. Вычисляемый скаляр тут необходим для того, чтобы конвертировать результат в ожидаемый на выходе тип int. Обратите внимание, что скалярное статистическое выражение потока — это практически единственный пример (я не могу придумать ещё один такой прямо сейчас) нелистового оператора, который может выдать строку на выход даже для пустого входного набора.

Легко показать, как работают и другие простые скалярные агрегатные функции, такие как: MIN, MAX и SUM. Также мы можем вычислять сразу несколько скалярных агрегатов, используя всего одно статистическое выражение потока:

select min(a),   max(b) from t

|—Stream Aggregate(DEFINE:([Expr1004]=MIN([t].[a]), [Expr1005]=MAX([t].[b])))

       |—Table Scan(OBJECT:([t]))

В этом плане считывается каждая строка таблицы «t», и отслеживается минимальное и максимальное значение столбцов «a» и «b» соответственно. Обратите внимание, что для агрегатов MIN и MAX нет необходимости приводить результат к нужному типу, так как типы этих агрегатов вычислены на основе типов столбцов «a» и «b».

Такие агрегаты, как AVG, фактически рассчитываются на основе двух других агрегатов: SUM и COUNT:

select avg(a) from t

|—Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [Expr1005]=(0) THEN NULL

                                            ELSE [Expr1006]/CONVERT_IMPLICIT(int,[Expr1005],0) END))

       |—Stream Aggregate(DEFINE:([Expr1005]=COUNT_BIG([t].[a]), [Expr1006]=SUM([t].[a])))

           |—Table Scan(OBJECT:([t]))

На этот раз во время вычисления скаляра также вычисляется среднее, для которого вычисляется сумма и общее число. Выражение CASE необходимо для исключения деления на ноль.

Хотя для SUM необходимость этого и не очевидна, но дополнительно выполняется подсчёт строк:

select sum(a)   from t

|—Compute Scalar(DEFINE:([Expr1004]=CASE WHEN [Expr1005]=(0) THEN NULL

                                             ELSE [Expr1006] END))

       |—Stream Aggregate(DEFINE:([Expr1005]=COUNT_BIG([t].[a]), [Expr1006]=SUM([t].[a])))

            |—Table Scan(OBJECT:([t]))

Выражение CASE использует общее число строк для гарантии того, что если на входе будет пустое множество, SUM возвратит вместо нуля — NULL.

Скалярный DISTINCT

Теперь давайСкалярный DISTINCT посмотрим, что случается, если ли мы добавим к агрегату ключевое слово DISTINCT:

select count(distinct a) from t

|—Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))

       |—Stream Aggregate(DEFINE:([Expr1007]=COUNT([t].[a])))

            |—Sort(DISTINCT ORDER BY:([t].[a] ASC))

                 |—Table Scan(OBJECT:([t]))

Этот запрос должен сосчитать только те строки, которые имеют уникальное для столбца «a» значение. Для устранения дубликатов строк с одинаковыми значениями в столбце «a» используется оператор сортировки. Дубли строк будут просто удалены из вывода, найти их не трудно, поскольку после сортировки полученного на входе набора дубликаты будут смежными записями.

Не все DISTINCT агрегаты требуют устранения дубликатов. Например, MIN и MAX ведут себя точно так же и без ключевого слова DISTINCT. Минимальные и максимальные значения в наборе остаются теми же самыми, не смотря на то, есть ли в наборе дубликаты или нет.

select min(distinct a), max(distinct b) from t

|—Stream Aggregate(DEFINE:([Expr1004]=MIN([t].[a]), [Expr1005]=MAX([t].[b])))

       |—Table Scan(OBJECT:([t]))

Если имеется уникальный индекс, устранение дубликатов тоже можно пропустить, так как индекс гарантирует, что дубликатов нет:

create unique   index ta on   t(a) select count(distinct a) from t drop index   t.ta

|—Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))

       |—Stream Aggregate(DEFINE:([Expr1007]=COUNT([t].[a])))

            |—Index Scan(OBJECT:([t].[ta]))

Несколько DISTINCT

Рассмотрим такой запрос:

select count(distinct a), count(distinct b) from t

Как было показано выше, мы можем вычислить «count(distinct a)», устраняя дубликаты строк для столбца «a». Точно так же мы можем вычислить «count(distinct b)», устраняя дубликаты строк для столбца «b». Но это при условии, что эти оба набора строк различны. Как можно вычислить оба дистинкта в одно и то же время? Ответ — мы не можем. Мы должны сначала вычислить одну составляющую результата, а потом другую, и затем нужно объединить оба результата в одну строку для вывода:

|—Nested Loops(Inner Join)

       |—Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1010],0)))

       |    |—Stream Aggregate(DEFINE:([Expr1010]=COUNT([t].[a])))

       |         |—Sort(DISTINCT ORDER BY:([t].[a] ASC))

       |              |—Table Scan(OBJECT:([t]))

       |—Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1011],0)))

            |—Stream Aggregate(DEFINE:([Expr1011]=COUNT([t].[b])))

                 |—Sort(DISTINCT ORDER BY:([t].[b] ASC))

                      |—Table Scan(OBJECT:([t]))

Для двух входных наборов выполняется подсчёт как в оригинальном запросе, и они соединяются вложенным циклом. Один входной набор удаляет дубликаты для столбца «a», другой для столбца «b».

Соединение вложенных циклов не имеет предиката соединения; это перекрестное соединение. Используя оба вводных набора, соединение вложенных циклов выдаёт одну строку с двумя скалярными агрегатами (результат перекрестного соединения — одна строка). Перекрестное соединение нужно только для того, чтобы «склеить» два столбца результата в одну строку.

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

В следующей статье, я напишу об агрегатах с предложением GROUP BY.


ссылка на оригинал статьи https://habr.com/ru/post/658469/

Разглядывая JTAG: идентификация

Каждый электронщик, работающий (или отдыхающий) с цифровыми микросхемами рано или поздно обязательно сталкивается с протоколом JTAG. Значительное количество материалов о данном протоколе содержит три раздела:

  1. Обширный экскурс в историю и рассказ о том, как стенд с летающими щупами и рентгеновская установка легко могут быть заменены отладчиком на 2-3 порядка дешевле их.

  2. Достаточно сжатое описание протокола JTAG (с картинкой его конечного автомата).

  3. Рассказ о том, что фирменный отладчик, а также программное обеспечение компании <COMPANY NAME> позволят почти без усилий протестировать почти любое устройство почти любой сложности и конфигурации.

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

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

  1. Микросхема с JTAG. Есть незначительное количество цифровых микросхем (в основном таким страдают процессоры), у которых JTAG не работает, пока ядро процессора не затактировано. Однако большинство микросхем, у которых вы сможете как-либо присоединиться к линиям TMS, TCK, TDI, TDO (если есть nTRST, то и к нему) подойдёт для данного исследования без дополнительных ограничений. У меня это будет небольшая ПЛИС «5M80ZE64C5N», знакомая некоторым читателям по предыдущему циклу.

  2. Программное обеспечение. Я буду рассказывать про «TopJTAG Probe». Он не перегружен высокоуровневым функционалом и имеет триальную версию.

  3. Отладчик JTAG. «TopJTAG Probe» позволяет использовать порядка 10 различных и достаточно распространённых отладчиков. Данный список несколько отличается от одного ПО к другому. Но базово, во многих пакетах, работающих с JTAG-ом, предусмотрена работа с микросхемой «FT2232». Эта популярная микросхема — мост между USB и широкой номенклатурой протоколов, в том числе и JTAG. В данном цикле я буду использовать отладчик на основе микросхемы «FT2232» и отладчик «Intel(Altera) USB-Blaster».

Соединим микросхему с отладчиком JTAG и подадим питание. Делая это, обратим внимание на несколько ключевых моментов:

  • В ряде демоплат соединение с отладчиком выполнено в виде разъёма с ключом. Если у вас так — то всё отлично. Если нет — придётся соединять порт JTAG с отладчиком отдельными проводами.

  • Если у порта JTAG микросхемы имеется линия nTRST (она может и отсутствовать), то для работы с JTAG её следует подтянуть к питанию.

  • Зачастую питание выходных трансляторов уровней отладчика JTAG выводят на отдельный контакт разъёма отладчика. Таким образом появляется возможность объединить шину питания микросхемы с шиной питания трансляторов и при отсутствующем питание микросхемы гарантировать отсутствие напряжения на линиях JTAG. Поэтому, помимо линий TMS/TCK/TDI/TDO/nTRST и земли, к разъёму отладчика скорее всего потребуется подключить и линию питания.

Считаем при помощи «TopJTAG Probe» идентификационный номер. Для этого:

  1. Выберем пункт меню «Scan → JTAG Connection…»

  2. В открывшемся окне выберем из списка подходящий программатор.

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

  4. Выберем пункт меню «Scan→Examine the Chain»

  5. В открывшемся окне появится идентификационный номер микросхемы. Для «5M80ZE64C5N» это будет h020A50DD.

Разберёмся, что есть что в этом номере. Согласно стандарту JTAG, в начале номера всегда должна стоять единица. Затем идут 11 бит кода производителя. Коды производителей указаны в таблицах стандарта JEP106 «Standard Manufacturer’s Identification Code». В стандарте есть 8 таблиц, по 256 строк каждая. «Altera», к примеру, находится в первой (b000) таблице. Похоже, что JEDEC убрал публичный доступ для этого стандарта, но его старые версии имеются на сторонних ресурсах. Оставшаяся часть номера содержит код конкретной марки микросхемы. Строго говоря, верхние 4 бита оставшейся части указывают на версию, но для подавляющего большинства микросхем в них стоят нули. Данный код марки указывается в технической документации на микросхему. В нашем случае — это стр.97 документа «MAX V Device Handbook».

Как же именно происходит передача идентификационного номера и какую именно последовательность сигналов необходимо подать на линии JTAG, чтобы считать этот номер?

Модуль JTAG внутри микросхемы на базовом уровне представляет из себя конечный автомат — систему, которая имеет 16 состояний. Переход между состояниями может произойти при восходящем фронте на линии TCK. А совершится ли он из одного конкретного состояния в другое, либо в третье, либо перехода не будет и система останется в прежнем состоянии — определяется логическим состоянием линии TMS в момент прохождения по линии TCK восходящего фронта, текущим состоянием автомата и схемой переходов.

Переходы между состояниями конечного автомата JTAG
Переходы между состояниями конечного автомата JTAG

Не углубляясь сейчас в детали, отметим три аспекта:

  1. Конечный автомат JTAG сделан таким образом, что из любого состояния возможно перейти в состояние «TEST LOGIC RESET», если пять раз подряд передать лог.1 по линии TMS (подавая при этом тактирующие импульсы по линии TCK). Выберете наугад состояние на схеме и посчитайте количество переходов по веткам с TMS=1 до состояния «TEST LOGIC RESET» 🙂

  2. Если модуль JTAG и был определённым образом настроен (каким — пока не важно), то переход в состояние «TEST LOGIC RESET» сбрасывает все настройки модуля в начальное состояние.

  3. Начальное состояние предусматривает готовность модуля JTAG выдать идентификационный номер.

Для старта передачи идентификационного номера нам нужно перевести конечный автомат в состояние «SHIFT DR» (до перехода в «SHIFT DR» модуль JTAG будет держать линию TDO в высокоимпедансном состоянии). Затем нужно удерживать конечный автомат в состоянии «SHIFT DR» 32 (или более) тактов TCK. В течение этих тактов идентификационный номер бит-за-битом будет передаваться по линии TDO из специального 32-х битного регистра модуля JTAG. При этом, в этот регистр бит-за-битом будут вдвигаться данные с линии TDI. Если мы с первого тактирующего импульса в состоянии «SHIFT DR» начнём последовательно подавать некий характерный паттерн по линии TDI, то через 32 импульса данный паттерн начнёт выходить через линию TDO.

Имеется тонкий момент, связанный с фронтами тактового сигнала, а также совместной работой модуля JTAG и отладчика. По стандарту, модуль JTAG считывает данные с линий TMS и TDI при восходящем фронте на линии TCK. Соответственно, отладчик должен заранее установить эти значения, а именно — на предшествующем нисходящем фронте TCK. Но так как микросхемы с JTAG могут собираться в цепочку, и каждая микросхема в цепи не знает, подключен ли её вход TDI непосредственно к отладчику или же к выходу TDO другой микросхемы, то модуль JTAG в любой микросхеме обязан изменять логическое значение на линии TDO также по нисходящему фронту TCK.

В связи с вышенаписанным, последовательность событий при начале выдачи данных из внутреннего регистра модуля JTAG в выход TDO будет такой:

  1. Очередной восходящий фронт по TCK приводит к изменению состояния конечного автомата на «SHIFT XX» внутри модуля JTAG.

  2. Синхронно со следующим нисходящим фронтом TCK модуль JTAG выдаст (скопирует) младший бит регистра в линию TDO. Одновременно с этим отладчик JTAG обязан выставить на линии TDI модуля JTAG бит, который предполагается поместить в регистр.

  3. Следующий восходящий фронт (если на TMS был установлен лог.0) приведёт к сдвигу регистра и «втягиванию» в него значения с TDI. Но так как значение младшего бита регистра уже выведено на линию TDO, то на этой стадии при сдвиге младший бит регистра просто исчезает.

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

Но мы поступим иначе, вернее — совсем наоборот! Попробуем написать такую программу, которая позволила бы микроконтроллеру имитировать устройство с модулем JTAG. А затем считаем при помощи отладчика JTAG и «TopJTAG Probe» идентификационный номер, который мы сами и зададим.

В качестве стенда будет использована плата «NUCLEO-F103RB» с микроконтроллером «STM32F103RBT6» и встроенным программатором «ST-Link».Сам проект написан в среде «STM32CubeIDE-1.7.0». А вся логика будет написана непосредственно в бесконечном цикле «while(1)» с использованием «bit-bang» («ногодрыга»).

Цвета навесных проводов соответствуют цветам линий JTAG с предыдущей картинки
Цвета навесных проводов соответствуют цветам линий JTAG с предыдущей картинки

Возможно, на этом месте у некоторых читателей возникла пара мыслей:

  1. «Пффф! Такие вещи надо делать на ПЛИС!»

  2. «Пффф! Такие вещи надо делать на прерываниях!»

По поводу первого комментария отмечу, что все инженеры, которым может пригодиться знание JTAG, знают «C/C++». Но не все из них также знают «Verilog/VHDL». Было бы неразумной дискриминацией большинства — не предоставить ему инструмент, которым оно привыкло пользоваться.

По поводу второго комментария отмечу, что не у всех читателей под рукой имеется именно «STM32F103RBT6». Вместо него может оказаться, к примеру, Ардуино. При портировании данного проекта на другое ядро, базовая настройка среды и проекта будет подразумевать лишь запуск примера «blinking led». Если же применять прерывания или какой-либо аппаратный блок периферии, то это создаст абсолютно ненужную (ввиду того, что подобные вещи действительно следует «делать на ПЛИС») привязку к конкретному микроконтроллеру. И, забегая вперёд, в данной статье после написания примера на «Си», будет продемонстрировано, как данный пример переписать на «SystemVerilog».

Прежде всего мы определим задействуемые линии GPIO. Для этого в начале файла «main.c» (также можно и в «main.h») запишем четыре директивы «define»:

#define JTMS_PIN GPIO_PIN_1 #define JTCK_PIN GPIO_PIN_15 #define JTDI_PIN GPIO_PIN_14 #define JTDO_PIN GPIO_PIN_13

Затем нам потребуется сконфигурировать данные выводы должным образом. Для этого нужно дописать в функцию «MX_GPIO_Init» следующий код:

Код инициализации GPIO для STM32
GPIO_InitStruct.Pin = JTMS_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);  GPIO_InitStruct.Pin = JTCK_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);  GPIO_InitStruct.Pin = JTDI_PIN; GPIO_InitStruct.Mode = GPIO_MODE_INPUT; GPIO_InitStruct.Pull = GPIO_NOPULL; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);  GPIO_InitStruct.Pin = JTDO_PIN; GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull = GPIO_NOPULL; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

Теперь перейдём непосредственно к логике программы. Добавим в функцию «main» перечисление состояний конечного автомата («finite state machine» — FSM) модуля JTAG, просто переписав их из схемы:

enum Jtag_fsm{ TEST_LOGIC_RESET, RUN_TEST_IDLE,  SELECT_DR_SCAN, CAPTURE_DR, SHIFT_DR, EXIT1_DR, PAUSE_DR, EXIT2_DR, UPDATE_DR,  SELECT_IR_SCAN, CAPTURE_IR, SHIFT_IR, EXIT1_IR, PAUSE_IR, EXIT2_IR, UPDATE_IR } jtag_fsm = TEST_LOGIC_RESET;

Затем добавим переменные, отвечающие за состояние того или иного вывода:

char tms_curr = 1; char tck_curr = 0; char tdi_curr = 0; char tdo_curr = 0;

Добавим в бесконечный цикл код, позволяющий оперативно узнавать состояние на линиях TMS/TCK/TDI через соответствующие переменные и столь же оперативно выдавать на линию TDO значение переменной «tdo_curr»:

while (1) { tms_curr = (GPIOB->IDR & JTMS_PIN ) ? 1 : 0; tck_curr = (GPIOB->IDR & JTCK_PIN)  ? 1 : 0; tdi_curr = (GPIOB->IDR & JTDI_PIN)  ? 1 : 0; if(tdo_curr)GPIOB->ODR |=  JTDO_PIN; elseGPIOB->ODR &= ~JTDO_PIN; }

В идеале, для TDO можно написать функцию, обеспечивающую (при необходимости) перевод данной линии в высокоимпедансное состояние. Но в первом приближении будет достаточно и приведённого кода.

Добавим в бесконечный цикл конструкцию, позволяющую обнаружить фронты на линии TCK. Для этого нам потребуется дополнительная переменная «tck_prev» (её нужно добавить к прочим переменным), содержащая значение «tck_curr» с предыдущей итерации бесконечного цикла:

if(tck_curr != tck_prev) { if(tck_curr) { //Реакция на восходящий фронт TCK } else  { //Реакция на нисходящий фронт TCK } } tck_prev = tck_curr;

Опишем переходы конечного автомата при помощи конструкции «switch-case», помещённой в ветвь восходящего фронта TCK. В нашем случае при помощи «switch» мы будет находить текущее состояние конечного автомата. Найдя его в определённом «case», мы будем однократно изменять (ну, либо оставим прежним) состояние конечного автомата в зависимости от состояния на линии TMS, то есть в зависимости от значения переменной «tms_curr»:

switch(jtag_fsm){ case TEST_LOGIC_RESET: if(tms_curr)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = RUN_TEST_IDLE; break; <...> case UPDATE_IR: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; } 
Полный код всех переходов
switch(jtag_fsm){ case TEST_LOGIC_RESET: if(tms_curr)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = RUN_TEST_IDLE; break; case RUN_TEST_IDLE: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; case SELECT_DR_SCAN: if(tms_curr)jtag_fsm = SELECT_IR_SCAN; elsejtag_fsm = CAPTURE_DR; break; case CAPTURE_DR: if(tms_curr)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; break; case SHIFT_DR: if(tms_curr)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; break; case EXIT1_DR: if(tms_curr)jtag_fsm = UPDATE_DR; elsejtag_fsm = PAUSE_DR; break; case PAUSE_DR: if(tms_curr)jtag_fsm = EXIT2_DR; elsejtag_fsm = PAUSE_DR; break; case EXIT2_DR: if(tms_curr)jtag_fsm = UPDATE_DR; elsejtag_fsm = SHIFT_DR; break; case UPDATE_DR: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; case SELECT_IR_SCAN: if(tms_curr)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = CAPTURE_IR; break; case CAPTURE_IR: if(tms_curr)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; break; case SHIFT_IR: if(tms_curr)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; break; case EXIT1_IR: if(tms_curr)jtag_fsm = UPDATE_IR; elsejtag_fsm = PAUSE_IR; break; case PAUSE_IR: if(tms_curr)jtag_fsm = EXIT2_IR; elsejtag_fsm = PAUSE_IR; break; case EXIT2_IR: if(tms_curr)jtag_fsm = UPDATE_IR; elsejtag_fsm = SHIFT_IR; break; case UPDATE_IR: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; } 

Для операций с идентификационным номером нам потребуется 32-битная константа с данным номером и 32-битная переменная (регистр):

const uint32_t ID_CODE = 0x0AA55003;//Придуманный нами номер uint32_t data_reg;

Добавим в ветвь восходящего фронта ещё одну конструкцию «switch-case» с выбором по jtag_fsm, в которой опишем логику работы регистров. Эту новую конструкцию необходимо вставить перед конструкцией «switch-case», отвечающей за переходы состояний конечного автомата. В новом «switch-case» создадим ветвь «TEST LOGIC RESET» в которой будем производить инициализацию регистров всякий раз, когда конечный автомат попадает в данное состояние:

switch(jtag_fsm){ case TEST_LOGIC_RESET: data_reg = ID_CODE;   break; }

Также добавим в этот «switch-case» сдвиг регистра в состоянии «SHIFT DR»:

case SHIFT_DR: data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31); break;

Теперь опишем логику работы с выходом TDO. Для этого создадим «switch-case» в ветке нисходящего фронта:

switch(jtag_fsm){ case TEST_LOGIC_RESET: tdo_curr = 0; break; case SHIFT_DR: tdo_curr = data_reg & ((uint32_t)0x1); break; case EXIT1_DR: tdo_curr = 0; break; }

В этом «switch-case», в ветке «SHIFT DR», происходит выдача младшего бита регистра в линию TDO. Линия TDO, по-хорошему должна переходить в высокоимпедансное состояние (HiZ) всегда, когда конечный автомат не пребывает в «SHIFT XX». Так как единственный выход из «SHIFT DR» — это «EXIT1 DR», то здесь и следует переключить линию в HiZ. Но ввиду того, что функционал высокоимпеданмного состояния TDO у нас не предусмотрен, оставим здесь некий компромиссный вариант в виде перевода линии в лог.0. Также следует на всякий случай перевести линию TDO в HiZ (у нас — в лог.0) при сбросе в состояние «TEST LOGIC RESET».

Мы обеспечили выдачу идентификационного номера через регистр данных — «Data Register» («DR»). Но программа «TopJTAG Probe» при идентификации проверяет также и наличие регистра инструкций — «Instruction Register» («IR»). Что это за регистр и зачем он нужен — пока не важно. На текущем этапе нам просто нужна ещё одна переменная-регистр длинной, скажем, 8 бит, инициализируемая нулём и способная к пробросу через себя данных с TDI в TDO. Для этого нужно объявить данную переменную:

uint8_t  inst_reg;

А затем дополнить конечные автоматы в ветке восходящего…

switch(jtag_fsm){ case TEST_LOGIC_RESET: data_reg = ID_CODE; inst_reg = 0; break; case SHIFT_DR: data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31); break; case SHIFT_IR: inst_reg = (inst_reg >> 1) | ((uint8_t)tdi_curr << 7); break; }

…и нисходящего фронта

switch(jtag_fsm){ case TEST_LOGIC_RESET: tdo_curr = 0; break; case SHIFT_DR: tdo_curr = data_reg & ((uint32_t)0x1); break; case EXIT1_DR: tdo_curr = 0; break; case SHIFT_IR: tdo_curr = inst_reg & ((uint8_t)0x1); break; case EXIT1_IR: tdo_curr = 0; break; }

Всё! Наш симулятор JTAG готов!

Полный код «main.c»
#include "main.h"  #define JTMS_PIN GPIO_PIN_1 #define JTCK_PIN GPIO_PIN_15 #define JTDI_PIN GPIO_PIN_14 #define JTDO_PIN GPIO_PIN_13  void SystemClock_Config(void); static void MX_GPIO_Init(void);  int main(void) { const uint32_t ID_CODE = 0x0AA55003;//Придуманный нами номер uint32_t data_reg; uint8_t  inst_reg;  char tms_curr = 1; char tck_curr = 0; char tck_prev = 0; char tdi_curr = 0; char tdo_curr = 0;  enum Jtag_fsm{ TEST_LOGIC_RESET, RUN_TEST_IDLE,  SELECT_DR_SCAN, CAPTURE_DR, SHIFT_DR, EXIT1_DR, PAUSE_DR, EXIT2_DR, UPDATE_DR,  SELECT_IR_SCAN, CAPTURE_IR, SHIFT_IR, EXIT1_IR, PAUSE_IR, EXIT2_IR, UPDATE_IR } jtag_fsm = TEST_LOGIC_RESET;  HAL_Init(); SystemClock_Config(); MX_GPIO_Init();  while (1) { tms_curr = (GPIOB->IDR & JTMS_PIN ) ? 1 : 0; tck_curr = (GPIOB->IDR & JTCK_PIN)  ? 1 : 0; tdi_curr = (GPIOB->IDR & JTDI_PIN)  ? 1 : 0; if(tdo_curr)GPIOB->ODR |=  JTDO_PIN; elseGPIOB->ODR &= ~JTDO_PIN;  if(tck_curr != tck_prev) { if(tck_curr) { switch(jtag_fsm){ case TEST_LOGIC_RESET: data_reg = ID_CODE; inst_reg = 0; break; case SHIFT_DR: data_reg = (data_reg >> 1) | ((uint32_t)tdi_curr << 31); break; case SHIFT_IR: inst_reg = (inst_reg >> 1) | ((uint8_t)tdi_curr << 7); break; }  switch(jtag_fsm){ case TEST_LOGIC_RESET: if(tms_curr)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = RUN_TEST_IDLE; break; case RUN_TEST_IDLE: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; case SELECT_DR_SCAN: if(tms_curr)jtag_fsm = SELECT_IR_SCAN; elsejtag_fsm = CAPTURE_DR; break; case CAPTURE_DR: if(tms_curr)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; break; case SHIFT_DR: if(tms_curr)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; break; case EXIT1_DR: if(tms_curr)jtag_fsm = UPDATE_DR; elsejtag_fsm = PAUSE_DR; break; case PAUSE_DR: if(tms_curr)jtag_fsm = EXIT2_DR; elsejtag_fsm = PAUSE_DR; break; case EXIT2_DR: if(tms_curr)jtag_fsm = UPDATE_DR; elsejtag_fsm = SHIFT_DR; break; case UPDATE_DR: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; case SELECT_IR_SCAN: if(tms_curr)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = CAPTURE_IR; break; case CAPTURE_IR: if(tms_curr)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; break; case SHIFT_IR: if(tms_curr)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; break; case EXIT1_IR: if(tms_curr)jtag_fsm = UPDATE_IR; elsejtag_fsm = PAUSE_IR; break; case PAUSE_IR: if(tms_curr)jtag_fsm = EXIT2_IR; elsejtag_fsm = PAUSE_IR; break; case EXIT2_IR: if(tms_curr)jtag_fsm = UPDATE_IR; elsejtag_fsm = SHIFT_IR; break; case UPDATE_IR: if(tms_curr)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; break; } } else { switch(jtag_fsm){ case TEST_LOGIC_RESET: tdo_curr = 0; break; case SHIFT_DR: tdo_curr = data_reg & ((uint32_t)0x1); break; case EXIT1_DR: tdo_curr = 0; break; case SHIFT_IR: tdo_curr = inst_reg & ((uint8_t)0x1); break; case EXIT1_IR: tdo_curr = 0; break; } } } if(tdo_curr)GPIOB->ODR |=  JTDO_PIN; elseGPIOB->ODR &= ~JTDO_PIN; tck_prev = tck_curr;   } }   void SystemClock_Config(void) {   RCC_OscInitTypeDef RCC_OscInitStruct = {0};   RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};    RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;   RCC_OscInitStruct.HSIState = RCC_HSI_ON;   RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;   RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;   RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSI_DIV2;   RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL16;   if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)   {     Error_Handler();   }    RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK                               |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;   RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;   RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;   RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;   RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;    if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)   {     Error_Handler();   } }  void Error_Handler(void) {   __disable_irq();   while (1)   {   } }  static void MX_GPIO_Init(void) {   GPIO_InitTypeDef GPIO_InitStruct = {0};    __HAL_RCC_GPIOC_CLK_ENABLE();   __HAL_RCC_GPIOD_CLK_ENABLE();   __HAL_RCC_GPIOA_CLK_ENABLE();   __HAL_RCC_GPIOB_CLK_ENABLE();    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);    GPIO_InitStruct.Pin = B1_Pin;   GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;   GPIO_InitStruct.Pull = GPIO_NOPULL;   HAL_GPIO_Init(B1_GPIO_Port, &GPIO_InitStruct);    GPIO_InitStruct.Pin = GPIO_PIN_5;   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;   GPIO_InitStruct.Pull = GPIO_NOPULL;   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;   HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);    HAL_NVIC_SetPriority(EXTI15_10_IRQn, 0, 0);   HAL_NVIC_EnableIRQ(EXTI15_10_IRQn);     //==== JTAG PINS INIT BEGIN ====   GPIO_InitStruct.Pin = JTMS_PIN;   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;   GPIO_InitStruct.Pull = GPIO_NOPULL;   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);    GPIO_InitStruct.Pin = JTCK_PIN;   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;   GPIO_InitStruct.Pull = GPIO_NOPULL;   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);    GPIO_InitStruct.Pin = JTDI_PIN;   GPIO_InitStruct.Mode = GPIO_MODE_INPUT;   GPIO_InitStruct.Pull = GPIO_NOPULL;   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);    GPIO_InitStruct.Pin = JTDO_PIN;   GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;   GPIO_InitStruct.Pull = GPIO_NOPULL;   GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;   HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);   //==== JTAG PINS INIT END ====  }

Есть, правда, одна существенная проблема. Частота синхроимпульсов по линии TCK с использованием отладчика JTAG «Intel(Altera) USB-Blaster» и ПО «TopJTAG Probe» составляет 6,25МГц. То есть период следования фронтов синхроимпульсов составляет 0,16мкс, а время между восходящим и нисходящим фронтом составляет 0,08мкс. При этом, задержка от фронта по TCK до выдачи данных с TDO для «NUCLEO-F103RB» и написанной выше программы составляет порядка 1мкс.

Однако, при использовании в «TopJTAG Probe» отладчика на основе микросхемы «FT2232H» появляется возможность ограничить максимальную частоту синхроимпульсов, скажем, десятью килогерцами, для которых период составляет гигантские 100мкс.

Скомпилируем получившуюся программу, запишем прошивку в микроконтроллер, соединим навесными проводами отладчик JTAG с демонстрационной платой. И наконец считаем код при помощи «TopJTAG Probe»:

Теперь попробуем сделать тоже самое, только на «SystemVerilog» для ПЛИС. Я использую демоплату «DE0-Nano» на базе ПЛИС «Cyclone IV» («EP4CE22F17C6N»).

Для начала создадим модуль верхнего уровня с тремя входами и одним выходом:

module main ( inputtms, inputtck, inputtdi, output regtdo); //Логика endmodule

После сборки проекта в планировщике выводов появится список из четырёх строк с названиями выводов модуля и возможностью их назначения выводам микросхемы ПЛИС. Распределив выводы, создадим в модуле параметр (нечто вроде константы в терминологии «SystemVerilog») и два регистра (нечто вроде переменных в терминологии «SystemVerilog»). В «SystemVerilog» при объявлении регистра, содержащего более чем один бит, необходимо явно указать общее количество бит. Установим такие же размеры регистров, как в примере на «Си»:

parameter ID_CODE= 32'h0AA55003; reg[31:0]data_reg; reg[ 7:0]inst_reg;

Затем создадим перечисление со списком состояний конечного автомата (почти как в «Си»):

enum {TEST_LOGIC_RESET, RUN_TEST_IDLE,  SELECT_DR_SCAN, CAPTURE_DR, SHIFT_DR, EXIT1_DR, PAUSE_DR, EXIT2_DR, UPDATE_DR,  SELECT_IR_SCAN, CAPTURE_IR, SHIFT_IR, EXIT1_IR, PAUSE_IR, EXIT2_IR, UPDATE_IR } jtag_fsm = TEST_LOGIC_RESET;

Для написания реакций на изменения уровней отдельных выводов в «SystemVerilog» применяется специальный блок «always». Создадим такой блок:

always @(posedge tck) begin //Логика end

Та логика, что будет описана внутри данного блока, будет однократно срабатывать каждый раз, когда по линии TCK в ПЛИС будет приходить восходящий фронт. Добавим в данный блок структуру «case»:

always @(posedge tck) begin case(jtag_fsm) XXX:begin if(tms)jtag_fsm = YYY; elsejtag_fsm = ZZZ; end endcase end

Как видно, синтаксическая конструкция «case» языка «SystemVerilog» позволяет в данном случае перенести код с «Си» на «SystemVerilog» практически без изменений:

Полный код структуры «case», описывающий логику переходов конечного автомата
case(jtag_fsm) TEST_LOGIC_RESET:begin if(tms)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = RUN_TEST_IDLE; end RUN_TEST_IDLE:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end SELECT_DR_SCAN:begin if(tms)jtag_fsm = SELECT_IR_SCAN; elsejtag_fsm = CAPTURE_DR; end CAPTURE_DR:begin if(tms)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; end SHIFT_DR:begin if(tms)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; end EXIT1_DR:begin if(tms)jtag_fsm = UPDATE_DR; elsejtag_fsm = PAUSE_DR; end PAUSE_DR:begin if(tms)jtag_fsm = EXIT2_DR; elsejtag_fsm = PAUSE_DR; end EXIT2_DR:begin if(tms)jtag_fsm = UPDATE_DR; elsejtag_fsm = SHIFT_DR; end UPDATE_DR:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end SELECT_IR_SCAN:begin if(tms)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = CAPTURE_IR; end CAPTURE_IR:begin if(tms)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; end SHIFT_IR:begin if(tms)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; end EXIT1_IR:begin if(tms)jtag_fsm = UPDATE_IR; elsejtag_fsm = PAUSE_IR; end PAUSE_IR:begin if(tms)jtag_fsm = EXIT2_IR; elsejtag_fsm = PAUSE_IR; end EXIT2_IR:begin if(tms)jtag_fsm = UPDATE_IR; elsejtag_fsm = SHIFT_IR; end UPDATE_IR:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end endcase

В «SystemVerilog» удобнее обращаться к отдельным битам регистра (по сути, регистр является массивом бит), удобнее производить конкатенацию («склеивание») отдельных регистров в один и удобнее устанавливать вывод в высокоимпедансное состояние. Описание реакции на нисходящий фронт TCK будет следующей:

always @(negedge tck) begin case(jtag_fsm) TEST_LOGIC_RESET:begin tdo= 1'bz;//Перевод tdo в HiZ end SHIFT_DR:begin tdo= data_reg[0]; end EXIT1_DR:begin tdo= 1'bz;//Перевод tdo в HiZ end SHIFT_IR:begin tdo= inst_reg[0]; end EXIT1_IR:begin tdo= 1'bz;//Перевод tdo в HiZ end endcase end

Остаётся добавить логику, осуществляющую сдвиг регистров. Здесь стоит сказать, что в языках описания аппаратуры имеются два типа присваивания. Не углубляясь в детали, отметим, что в данном примере используется неблокирующее присваивание. И чтобы данный код работал с именно таким присваиванием, нам потребуется создать ещё один, дополнительный блок «always», реагирующий на восходящий фронт:

always @(posedge tck) begin case(jtag_fsm) TEST_LOGIC_RESET:begin data_reg= ID_CODE; inst_reg= 0; end SHIFT_DR:begin; data_reg= {tdi,data_reg[31:1]};//конкатенация tdi и сдвинутого регистра end SHIFT_IR:begin inst_reg= {tdi,inst_reg[7:1]};//конкатенация tdi и сдвинутого регистра end endcase end
Полный код модуля «main»
module main ( inputtms, inputtck, inputtdi, output reg tdo);  parameter ID_CODE= 32'h0AA55003;  reg[31:0]data_reg; reg[ 7:0]inst_reg;  enum {TEST_LOGIC_RESET,    RUN_TEST_IDLE,     SELECT_DR_SCAN,    CAPTURE_DR,    SHIFT_DR,    EXIT1_DR,    PAUSE_DR,    EXIT2_DR,    UPDATE_DR,     SELECT_IR_SCAN,    CAPTURE_IR,    SHIFT_IR,    EXIT1_IR,    PAUSE_IR,    EXIT2_IR,    UPDATE_IR } jtag_fsm = TEST_LOGIC_RESET;  always @(posedge tck) begin case(jtag_fsm) TEST_LOGIC_RESET:begin if(tms)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = RUN_TEST_IDLE; end RUN_TEST_IDLE:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end SELECT_DR_SCAN:begin if(tms)jtag_fsm = SELECT_IR_SCAN; elsejtag_fsm = CAPTURE_DR; end CAPTURE_DR:begin if(tms)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; end SHIFT_DR:begin if(tms)jtag_fsm = EXIT1_DR; elsejtag_fsm = SHIFT_DR; end EXIT1_DR:begin if(tms)jtag_fsm = UPDATE_DR; elsejtag_fsm = PAUSE_DR; end PAUSE_DR:begin if(tms)jtag_fsm = EXIT2_DR; elsejtag_fsm = PAUSE_DR; end EXIT2_DR:begin if(tms)jtag_fsm = UPDATE_DR; elsejtag_fsm = SHIFT_DR; end UPDATE_DR:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end SELECT_IR_SCAN:begin if(tms)jtag_fsm = TEST_LOGIC_RESET; elsejtag_fsm = CAPTURE_IR; end CAPTURE_IR:begin if(tms)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; end SHIFT_IR:begin if(tms)jtag_fsm = EXIT1_IR; elsejtag_fsm = SHIFT_IR; end EXIT1_IR:begin if(tms)jtag_fsm = UPDATE_IR; elsejtag_fsm = PAUSE_IR; end PAUSE_IR:begin if(tms)jtag_fsm = EXIT2_IR; elsejtag_fsm = PAUSE_IR; end EXIT2_IR:begin if(tms)jtag_fsm = UPDATE_IR; elsejtag_fsm = SHIFT_IR; end UPDATE_IR:begin if(tms)jtag_fsm = SELECT_DR_SCAN; elsejtag_fsm = RUN_TEST_IDLE; end endcase end  always @(posedge tck) begin case(jtag_fsm) TEST_LOGIC_RESET:begin data_reg= ID_CODE; inst_reg= 0; end SHIFT_DR:begin; data_reg= {tdi,data_reg[31:1]}; end SHIFT_IR:begin inst_reg= {tdi,inst_reg[7:1]}; end endcase end  always @(negedge tck) begin case(jtag_fsm) TEST_LOGIC_RESET:begin tdo= 1'bz; end SHIFT_DR:begin tdo= data_reg[0]; end EXIT1_DR:begin tdo= 1'bz; end SHIFT_IR:begin tdo= inst_reg[0]; end EXIT1_IR:begin tdo= 1'bz; end endcase end  endmodule

Следует отметить (или напомнить), что код на «SystemVerilog» синтезируется в карту соединений между элементами ПЛИС и выполняется не последовательностью команд, как в «Си», а, по сути, одновременно. Даже без дополнительной оптимизации, данный код на демоплате «DE0-Nano» имеет задержку между приходом фронта по линии TCK и выдачей очередного бита по линии TDO всего 0,008мкс.

Разобравшись с работой конечного автомата JTAG на примере выполнения команды чтения идентификационного номера можно перейти к прочим командам. Их применение напрямую связано с языком BSDL, на котором мы в следующий раз напишем файл описания модуля JTAG для воображаемой микросхемы, а также напишем на «Си» и «SystemVerilog» код, имитирующий работу данной микросхемы.


ссылка на оригинал статьи https://habr.com/ru/post/658477/

Использование Gatling. Разбираемся в тестировании HTTP

Всем привет! Это команда тестирования производительности Тинькофф, и мы продолжаем цикл статей о Gatling. 

В предыдущей статье мы рассмотрели базовые возможности инструмента Gatling, узнали, как быстро создать шаблон gatling-проекта, и познакомились с новыми функциями библиотеки gatling-picatinny. Сегодня расскажем, как при помощи этих инструментов создать проект для тестирования HTTP-протокола.

Дисклеймер

На момент написания статьи используемые плагины поддерживают версию Gatling не выше 3.7.6. Для получения работающего скрипта в будущем достаточно обновить версии плагинов в соответствии с их документацией.

Что такое HTTP-протокол

HyperText Transfer Protocol — протокол прикладного уровня передачи данных. Изначально — в виде гипертекстовых документов в формате HTML. Сейчас протокол используют для передачи произвольных данных. 

Основа HTTP 1.1 — технология «клиент — сервер». В этой схеме есть:

  • потребители (клиенты), которые инициируют соединение и посылают запрос;

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

Gatling также имеет поддержку HTTP/2, но она пока экспериментальная. Как только добавят полноценную поддержку, мы обязательно напишем про это статью.

Как разработать скрипт для тестирования

В качестве тестового сервиса будем использовать сайт Computer Database. Пожалуйста, не подавайте на него нагрузку — используйте сайт только для отладки ваших скриптов.

Для скриптов реализуем такие запросы: вход на главную страницу и создание нового компьютера. Мы не будем разрабатывать проект с нуля, а используем уже готовый шаблон и с его помощью создадим проект ​​myhttpservice. Процесс создания мы описывали в предыдущей статье. А теперь разберемся, как создать нужный нам скрипт для тестирования HTTP-протокола.

Шаг 1. Обновляем зависимости

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

В файле project/Dependencies.scala проверьте актуальную версию Gatling и версию gatlingPicatinny.

 lazy val gatling: Seq[ModuleID]          = Seq(     "io.gatling.highcharts" % "gatling-charts-highcharts",     "io.gatling"            % "gatling-test-framework",   ).map(_ % "[current_version]" % Test)    lazy val gatlingPicatinny: Seq[ModuleID] = Seq(     "ru.tinkoff" %% "gatling-picatinny",   ).map(_ % "[current_version]")

В файле build.sbt проверьте, что версия scala поддерживается текущей версией Gatling.

scalaVersion := "[support_version]"

Загрузите новые зависимости в проект, запустив команду в консоли. Готово — вы обновили зависимости.

sbt update

Шаг 2. Меняем переменные

В файле src/test/resources/simulation.conf хранятся дефолтные переменные для запуска. Для нашего сервиса нужно обновить только baseUrl.

baseUrl: "http://computer-database.gatling.io"

Шаг 3. Создаем Action для публикации сообщений

В директории сases создайте новый файл для объекта HttpActions. В нем опишите все необходимые действия. Например, давайте создадим два запроса: get и post. Добавим к запросам параметры, тело запроса и проверки. Проверки помогут понять, успешно ли завершился запрос. Для большего разнообразия параметризуем запрос createNewComputer. Параметры «${randomComputerName}», «#{introduced}», «#{discontinued}» позволят использовать значения, полученные из Feeder. Параметр «#{company}» мы получаем из ответа на предыдущий запрос pressButtonAddNewComputer.

package ru.tinkoff.load.myhttpservice.cases  import io.gatling.core.Predef. import io.gatling.http.Predef. import io.gatling.http.request.builder.HttpRequestBuilder  object Actions {   val openMainPage: HttpRequestBuilder =    // Указываем имя запроса    http("Open main page")      // Указываем тип запроса (метод) и эндпоинт      .get("/")      .check(        // Проверяем, что в ответе пришел ОК        // (по умолчанию Gatling проверяет ответы на все успешные коды возврата: 2xx, 3xx)        status is 200,      )   val pressButtonAddNewComputer: HttpRequestBuilder =    http("pressButtonAddNewComputer")      .get("/computers/new")      .check(        status is 200,      )      .check(        // Забираем из ответа случайную компанию и сохраняем значение         // в переменной 'company'        regex("<option value=\"(.+?)\"").findRandom.saveAs("company"),      )   val createNewComputer: HttpRequestBuilder =    http("createNewComputer")      .post("/computers")       // Указываем параметры запроса. Обратите внимание на значение,        // таким образом мы можем параметризовать запросы.      .formParam("name", "#{randomComputerName}")      .formParam("introduced", "#{introduced}")      .formParam("discontinued", "#{discontinued}")      // Тут вместо #{company} подставится значение из предыдущего запроса                                           .formParam("company", "#{company}") }

Шаг 4. Используем Feeders

Узнать о Feeders побольше можно из предыдущей статьи. В нашем примере мы будем использовать фидеры из подключаемой библиотеки gatling-picatinny. Для этого в директории myhttpservice создайте новую директорию feeders, а в ней — object Feeders.

package ru.tinkoff.load.myhttpservice.feeders  import io.gatling.core.feeder._ import ru.tinkoff.gatling.feeders._  object Feeders {    // Для имени компьютера будем использовать случайную строку с нужным алфавитом.   val randomComputerName: Feeder[String] =     RandomRangeStringFeeder("randomComputerName", alphabet = ('A' to 'Z').mkString)    // Используем фидер для создания случайной даты   val introducedDate: Feeder[String] = RandomDateFeeder("introduced")    // Создаем случайную дату со сдвигом относительно 'introduced' даты   val discontinuedDate: Feeder[String] = RandomDateRangeFeeder("introduced", "discontinued", 3)    // Объединяем фидеры в одну переменную для удобства                                                     val feeders: Feeder[Any] = randomComputerName ** introducedDate ** discontinuedDate  }

Шаг 5. Пишем сценарий теста

Для этого измените код в CommonScenario. Опишите класс CommonScenario, в котором создаете сценарий — порядок выполнения определенных действий.

package ru.tinkoff.load.myhttpservice.scenarios  import io.gatling.core.Predef._ import io.gatling.core.structure.ScenarioBuilder import ru.tinkoff.load.myhttpservice.cases.Actions._ import ru.tinkoff.load.myhttpservice.feeders.Feeders._  /* Объект-компаньон для класса CommonScenario, по сути синтаксический сахар, чтобы можно было вызвать сценарий таким образом CommonScenario(), вместо new CommonScenario().scn */ object CommonScenario {   def apply(): ScenarioBuilder = new CommonScenario().scn }  class CommonScenario {    // Создаем сценарий и его имя   val scn: ScenarioBuilder = scenario("CommonScenario")     // Подключаем наши фидеры     .feed(feeders)     // Подключаем наши запросы     .exec(pressButtonAddNewComputer)     .exec(createNewComputer) }

Шаг 6. Создаем описание HTTP-протокола

По умолчанию в проекте из шаблона g8 уже настроен протокол HTTP. Для нашего скрипта в файле myhttpserviсe.scala опишем еще один протокол. 

package ru.tinkoff.load  import io.gatling.core.Predef._ import io.gatling.http.Predef._ import io.gatling.http.protocol.HttpProtocolBuilder // Подключаем стандартные переменные из gatling picatinny import ru.tinkoff.gatling.config.SimulationConfig._  package object myhttpservice {    val httpProtocol: HttpProtocolBuilder = http     // Используем стандартную переменную из gatling picatinny, значение которой подтянется из simulation.conf     .baseUrl(baseUrl)     // Базовые заголовки. Для большинства кейсов этого будет достаточно и ничего менять не нужно.                          .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")     .acceptEncodingHeader("gzip, deflate")     .acceptLanguageHeader("en-US,en;q=0.5")     .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")     // Если требуется добавить авторизацию, можно использовать специальные методы.     .authorizationHeader("Bearer token")     // Или определить свой заголовок     .header("customHeader", "value") }

Шаг 7. Подготавливаем нагрузочные тесты

В файле Debug.scala раскомментируйте строку с proxy для локальной отладки. Перед заливкой в VCS нужно либо убрать proxy, либо закомментировать его. 

package ru.tinkoff.load.myhttpservice  import io.gatling.core.Predef._ import io.gatling.http.Predef._ import ru.tinkoff.gatling.config.SimulationConfig._ import ru.tinkoff.load.myhttpservice.scenarios.CommonScenario  class Debug extends Simulation {    setUp(     // Запускаем наш сценарий     CommonScenario()       // Запускать будет один пользователь - одну итерацию       .inject(atOnceUsers(1)),   ).protocols(     // Работа будет проходить по протоколу, который описан в конфигурации httpProtocol     httpProtocol       // Настраиваем прокси для отладки запросов, используем для этого fiddler        // или charles (8888 стандартный порт для прокси)       .proxy(Proxy("localhost", 8888).httpsPort(8888))                                                   )     // Максимальное время теста равно testDuration, если тест не завершится за меньшее время,      // он будет остановлен автоматически     .maxDuration(testDuration) }

Для MaxPerformance- и Stability-тестов ничего менять не нужно. Подойдут дефолтные настройки для HTTP.

Шаг 8. Запускаем Debug-тест

Для запуска используйте GatlingRunner. После выполнения скрипта можно посмотреть консоль fiddler или charles и увидеть наши запросы.

package ru.tinkoff.load.myhttpservice  import io.gatling.app.Gatling import io.gatling.core.config.GatlingPropertiesBuilder  object GatlingRunner {    def main(args: Array[String]): Unit = {      // Указывает имя симуляции Debug, либо какой-то другой,      // например, MaxPerformance     val simulationClass = classOf[Debug].getName      val props = new GatlingPropertiesBuilder     props.simulationClass(simulationClass)      Gatling.fromMap(props.build)   }  }

Так, например, выглядят наши запросы в charles.

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

sbt "Gatling / testOnly ru.tinkoff.load.myhttpservice.Debug"

Ниже — пример успешного запуска скрипта.

Simulation ru.tinkoff.load.myhttpservice.Debug started...                               ================================================================================ 2022-01-27 11:44:22                                           0s elapsed ---- Requests ------------------------------------------------------------------ > Global                                                   (OK=3      KO=0     ) > pressButtonAddNewComputer                                (OK=1      KO=0     ) > createNewComputer                                        (OK=1      KO=0     ) > createNewComputer Redirect 1                             (OK=1      KO=0     )  ---- CommonScenario ------------------------------------------------------------ [##########################################################################]100%           waiting: 0      / active: 0      / done: 1      ================================================================================  Simulation ru.tinkoff.load.myhttpservice.Debug completed in 0 seconds Parsing log file(s)... Parsing log file(s) done Generating reports...  ================================================================================ ---- Global Information -------------------------------------------------------- > request count                                          3 (OK=3      KO=0     ) > min response time                                    153 (OK=153    KO=-     ) > max response time                                    319 (OK=319    KO=-     ) > mean response time                                   209 (OK=209    KO=-     ) > std deviation                                         78 (OK=78     KO=-     ) > response time 50th percentile                        154 (OK=154    KO=-     ) > response time 75th percentile                        237 (OK=237    KO=-     ) > response time 95th percentile                        303 (OK=303    KO=-     ) > response time 99th percentile                        316 (OK=316    KO=-     ) > mean requests/sec                                      3 (OK=3      KO=-     ) ---- Response Time Distribution ------------------------------------------------ > t < 800 ms                                             3 (100%) > 800 ms < t < 1200 ms                                   0 (  0%) > t > 1200 ms                                            0 (  0%) > failed                                                 0 (  0%) ================================================================================

Заключение

Мы рассказали, как быстро и легко создать проект из нашего шаблона и написать скрипты для HTTP-протокола. В этом нам помогла наша библиотека Picatinny.

В следующих статьях поговорим о том, как написать скрипты для протоколов gRPC,  AMQP, JDBC и kafka.

Полезные ссылки

  1. Подробнее о сессии в Gatling.

  2. Подробнее о составлении HTTP-запросов в Gatling.

  3. Подробнее о фидерах в Gatling.

  4. Подробнее о настройке протокола HTTP в Gatling.

  5. Проект Gatling из примеров этой статьи.


ссылка на оригинал статьи https://habr.com/ru/company/tinkoff/blog/658479/

Как проверить данные во фрейме Pandas с помощью Pandera

Убедитесь, что данные соответствуют ожиданиям

В науке о данных важно тестировать не только функции, но и данные, чтобы убедиться, что они работают так, как вы ожидали. Материалом о простой библиотеке Pandera для валидации фреймов данных Pandas делимся к старту флагманского курса по Data Science.


Чтобы установить Pandera, в терминале наберите:

pip install pandera

Введение

Начнём с простого набора данных, чтобы понять, как работает Pandera:

import pandas as pd  fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", "Aldi"],         "price": [2, 1, 3, 4],     } )  fruits

Представьте: ваш менеджер сказал вам, что в наборе данных могут храниться только определённые фрукты, а значение их цены должно быть меньше 4:

available_fruits = ["apple", "banana", "orange"] nearby_stores = ["Aldi", "Walmart"]

Проверка данных вручную может занять много времени, особенно когда их много. Есть ли способ автоматизировать проверку? Да, здесь и пригодится Pandera:

  • создадим тесты всего набора данных с помощью DataFrameSchema;

  • тесты для каждой колонки — при помощи Column;

  • тип теста определим при помощи Check.

import pandera as pa from pandera import Column, Check  schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store": Column(str, Check.isin(nearby_stores)),         "price": Column(int, Check.less_than(4)),     } ) schema.validate(fruits)
SchemaError: <Schema Column(name=price, type=DataType(int64))> failed element-wise validator 0: <Check less_than: less_than(4)> failure cases:    index  failure_case 0      3             4

Поясню этот код:

  • "name": Column(str, Check.isin(available_fruits)) проверяет, имеет ли столбец name тип string и все ли значения столбца name находятся внутри указанного списка;

  • "price": Column(int, Check.less_than(4)) проверяет, все ли значения в столбце price имеют тип int и меньше 4;

  • не все значения в столбце price меньше 4, поэтому тест не проходит.

Другие встроенные методы Checks вы найдёте здесь.

Настраиваемые проверки

Проверки можно писать и через лямбда-выражения. В коде ниже Check(lambda price: sum(price) < 20) проверяет, меньше ли 20 сумма в price.

schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store": Column(str, Check.isin(nearby_stores)),         "price": Column(             int, [Check.less_than(5), Check(lambda price: sum(price) < 20)]         ),     } ) schema.validate(fruits)

SchemaModel

Когда тесты сложные, чище код сделают не словари, а классы данных. К счастью, Pandera позволяет создавать тесты с классами данных.

from pandera.typing import Series  class Schema(pa.SchemaModel):     name: Series[str] = pa.Field(isin=available_fruits)     store: Series[str] = pa.Field(isin=nearby_stores)     price: Series[int] = pa.Field(le=5)      @pa.check("price")     def price_sum_lt_20(cls, price: Series[int]) -> Series[bool]:         return sum(price) < 20  Schema.validate(fruits)

Декоратор валидации

Проверка ввода

Как тестировать входные значения функции? Прямолинейный подход — добавить schema.validate(input) прямо в функцию:

fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", "Aldi"],         "price": [2, 1, 3, 4],     } )  schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store": Column(str, Check.isin(nearby_stores)),         "price": Column(int, Check.less_than(5)),     } )   def get_total_price(fruits: pd.DataFrame, schema: pa.DataFrameSchema):     validated = schema.validate(fruits)     return validated["price"].sum()   get_total_price(fruits, schema)

Но он осложняет тестирование. Функция get_total_price имеет аргументы fruits and schema, а значит, в тест функции нужно включить оба:

def test_get_total_price():     fruits = pd.DataFrame({'name': ['apple', 'banana'], 'store': ['Aldi', 'Walmart'], 'price': [1, 2]})          # Need to include schema in the unit test     schema = pa.DataFrameSchema(         {             "name": Column(str, Check.isin(available_fruits)),             "store": Column(str, Check.isin(nearby_stores)),             "price": Column(int, Check.less_than(5)),         }     )     assert get_total_price(fruits, schema) == 3

Функция test_get_total_price проверяет и данные, и функцию. Модульный тест должен проверять только одну вещь, поэтому включение проверки данных внутри функции — не идеальное решение.

Эту проблему Pandera решает декоратором check_input. Аргумент декоратора применяется в валидации входных значений:

from pandera import check_input  @check_input(schema) def get_total_price(fruits: pd.DataFrame):     return fruits.price.sum()  get_total_price(fruits)

Если входное значение некорректно, Pandera поднимает исключение до обработки значения в функции:

fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", "Aldi"],         "price": ["2", "1", "3", "4"],     } )  @check_input(schema) def get_total_price(fruits: pd.DataFrame):     return fruits.price.sum()  get_total_price(fruits)
SchemaError: error in check_input decorator of function 'get_total_price': expected series 'price' to have type int64, got object

Такая проверка до обработки в функции экономит много времени.

Проверка вывода

Для проверки вывода можно использовать декоратор check_output:

from pandera import check_output  fruits_nearby = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", "Aldi"],         "price": [2, 1, 3, 4],     } )  fruits_faraway = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Whole Foods", "Whole Foods", "Schnucks", "Schnucks"],         "price": [3, 2, 4, 5],     } )  out_schema = pa.DataFrameSchema(     {"store": Column(str, Check.isin(["Aldi", "Walmart", "Whole Foods", "Schnucks"]))} )   @check_output(out_schema) def combine_fruits(fruits_nearby: pd.DataFrame, fruits_faraway: pd.DataFrame):     fruits = pd.concat([fruits_nearby, fruits_faraway])     return fruits   combine_fruits(fruits_nearby, fruits_faraway)

Проверка ввода и вывода

Проверить входные и выходные данные можно с помощью декоратора check_io:

from pandera import check_io  in_schema = pa.DataFrameSchema({"store": Column(str)})  out_schema = pa.DataFrameSchema(     {"store": Column(str, Check.isin(["Aldi", "Walmart", "Whole Foods", "Schnucks"]))} )   @check_io(fruits_nearby=in_schema, fruits_faraway=in_schema, out=out_schema) def combine_fruits(fruits_nearby: pd.DataFrame, fruits_faraway: pd.DataFrame):     fruits = pd.concat([fruits_nearby, fruits_faraway])     return fruits   combine_fruits(fruits_nearby, fruits_faraway)

Другие аргументы проверки столбцов

Null

По умолчанию Pandera выдаёт ошибку, если в проверяемом столбце есть Null. Если нулевые значения допустимы, в класс Column добавьте nullable=True:

import numpy as np  fruits = fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", np.nan],         "price": [2, 1, 3, 4],     } )  schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store": Column(str, Check.isin(nearby_stores), nullable=True),         "price": Column(int, Check.less_than(5)),     } ) schema.validate(fruits)

Дубликаты

По умолчанию дубликаты допустимы. Чтобы они поднимали исключение, добавьте аргумент allow_duplicates=False:

schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store": Column(             str, Check.isin(nearby_stores), nullable=True, allow_duplicates=False         ),         "price": Column(int, Check.less_than(5)),     } ) schema.validate(fruits)
SchemaError: series 'store' contains duplicate values: {2: 'Walmart'}

Преобразование типов данных

Аргумент coerce=True изменяет тип данных столбца, если тип не удовлетворяет условию проверки.

В коде ниже тип данных цены изменён с целого на строку:

fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store": ["Aldi", "Walmart", "Walmart", "Aldi"],         "price": [2, 1, 3, 4],     } )  schema = pa.DataFrameSchema({"price": Column(str, coerce=True)}) validated = schema.validate(fruits) validated.dtypes
name     object store    object price    object dtype: object

Сопоставление шаблонов

Что, если мы хотим изменить все столбцы, которые начинаются со слова store?

favorite_stores = ["Aldi", "Walmart", "Whole Foods", "Schnucks"]  fruits = pd.DataFrame(     {         "name": ["apple", "banana", "apple", "orange"],         "store_nearby": ["Aldi", "Walmart", "Walmart", "Aldi"],         "store_far": ["Whole Foods", "Schnucks", "Whole Foods", "Schnucks"],     } )

Pandera позволяет нам применять одни и те же проверки к нескольким столбцам с определённым шаблоном, вот так: regex=True:

schema = pa.DataFrameSchema(     {         "name": Column(str, Check.isin(available_fruits)),         "store_+": Column(str, Check.isin(favorite_stores), regex=True),     } ) schema.validate(fruits)

Экспорт и загрузка из файла YAML

Экспорт в YAML

YAML — отличный способ показать свои тесты коллегам, не знающим Python. Сохранить все проверки в файле YAML можно с помощью метода schema.to_yaml():

from pathlib import Path  # Get a YAML object yaml_schema = schema.to_yaml()  # Save to a file f = Path("schema.yml") f.touch() f.write_text(yaml_schema)

Файл schema.yml должен выглядеть примерно так:

schema_type: dataframe version: 0.7.0 columns:   name:     dtype: str     nullable: false     checks:       isin:       - apple       - banana       - orange     allow_duplicates: true     coerce: false     required: true     regex: false   store:     dtype: str     nullable: true     checks:       isin:       - Aldi       - Walmart     allow_duplicates: false     coerce: false     required: true     regex: false   price:     dtype: int64     nullable: false     checks:       less_than: 5     allow_duplicates: true     coerce: false     required: true     regex: false checks: null index: null coerce: false strict: false

Загрузка из YAML

Чтобы загрузить файл, используйте pa.io.from_yaml(yaml_schema):

with f.open() as file:     yaml_schema = file.read()  schema = pa.io.from_yaml(yaml_schema)

Заключение

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

А мы поможем вам прокачать навыки или с самого начала освоить профессию, востребованную в любое время:

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий


ссылка на оригинал статьи https://habr.com/ru/company/skillfactory/blog/658473/