Ad-social Bot

Smmok Bot

Vkserfing Bot

Vkstorm Bot

Vktarget Bot

Все программы

Запись опубликована: 23.02.2018

Привет, меня зовут Pavel Germanika

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


 

Может ли человек, проживая в унылом бесперспективном городе, чего-то добиться, имея под рукой лишь старый ноутбук и интернет? Можно ли без всяких офисов, начальных капиталов построить компанию, приносящую неплохую прибыль? Ответ на этот вопрос вы сможете узнать в моем блоге. Я Pavel Germanika, добро пожаловать!

Всю свою жизнь я прожил в мечтах. Это было какое-то полугипнотическое состояние. Я любил гулять по улицам или лежать на диване, представляя себя успешным человеком. Будь то бизнесменом, известным футболистом, ученым, которому вручают нобелевскую премию и прочими персонажами. Любые дела я всегда откладывал на потом. Все жизненные перспективы я старался не замечать. Думаю, многие люди со мной схожи.

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

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

Мой Блог — это реалити-шоу обычного заурядного парня, который пытается добиться успеха с полного нуля. Никакого начального капитала, никаких знакомств, никаких особых способностей. Только идеи, анализ и компьютер. Я хочу быть максимально открытым и делиться с вами всеми своими мыслями и результатами работы.

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

Любой из вас может связаться со мной. Я рад каждому! Пишите на мой Telegram.

Метки: ,





Запись опубликована: 22.05.2019

За всё ответишь! Consumer Driven Contracts глазами разработчика

В этой статье мы расскажем про проблемы, которые решает Consumer Driven Contracts, покажем как это применять на примере Pact с Node.js и Spring Boot. И расскажем про ограничения этого подхода.

Проблематика

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

  • Часто ошибочно считают, что интеграционная среда не должна быть отказоустойчивой. SLA, гарантии для таких сред редко проговаривают, но если она недоступна — командам приходится либо задерживать релизы, либо надеяться на лучшее и идти в бой без тестов. Хотя все знают, что надежда — это не стратегия. А новомодные инфраструктурные технологии только усложняют работу с интеграционными средами.
  • Отдельная боль — работа с тестовыми данными. Многие сценарии требуют определенного состояния системы, фикстуры. Насколько близкими они должны быть к боевым данным? Как приводить их к актуальному состоянию перед тестом и чистить после завершения?
  • Тесты слишком нестабильные. И не только из-за инфраструктуры, которую мы упомянули в первом пункте. Тест может упасть, потому что соседняя команда запустила свои проверки, которые сломали ожидаемое состояние системы! Многие false negative проверки, flaky-тесты заканчивают свою жизнь в @Ignored. Также разные части интеграции могут поддерживаться разными командами. Выкатили новый release candidate с ошибками — сломали всех потребителей. Кто-то решает эту проблему выделенными тестовыми контурами. Но ценой умножения стоимости поддержки.
  • Такие тесты занимают много времени. Даже с учетом автоматизации, результаты можно ждать часами.
  • А в довершении, если тест действительно справедливо упал, то далеко не всегда получается сразу найти причину проблемы. Она может скрываться глубоко за слоями интеграций. Или может быть следствием неожиданной комбинации состояний множества компонентов системы.

Стабильные тесты в интеграционной среде требуют серьезного вложения со стороны QA, dev и даже ops. Недаром они находятся на самой верхушке тестовой пирамиды. Такие тесты полезны, но экономика ресурсов не позволяет проверять ими все подряд. Основной источник их стоимости — это окружение.

Ниже по той же пирамиде находятся другие тесты, в которых мы размениваем достоверность на меньшие головные боли поддержки — с помощью изоляции проверок. Чем гранулярнее, меньше по масштабу проверка, тем меньше зависимость от внешней среды. На самом низу пирамиды находятся unit-тесты. Мы проверяем отдельные функции, классы, мы оперируем уже не столько бизнес-семантикой, сколько конструкциями конкретной реализации. Эти тесты дают быструю обратную связь.

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

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

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

Более того, документация обычно движется downstream — клиенты используют спеки сервисов (при этом клиентом сервиса может быть и другой сервис). Она не выражает того, как потребители используют данные, какие данные вообще нужны, какие допущения они делают для этих данных. Следствием такого незнания является закон Хайрама.

Хайрам Райт долгое время разрабатывал инструменты общего пользования внутри Google и наблюдал, как самые незначительные изменения могут вызвать поломки у клиентов, которые использовали неявные (недокументированные) особенности его библиотек. Такая скрытая связность усложняет эволюцию API.
Перечисленные проблемы в некоторой степени можно решить с помощью Consumer Driven Contracts. Как и у любого подхода и инструмента, у него есть область применимости и стоимость, которые мы тоже рассмотрим. Реализации этого подхода достигли достаточного уровня зрелости, чтобы пробовать на своих проектах.

Что такое CDC?

Три ключевых элемента:

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

Таким образом, контракт — исполняемый. И основная особенность подхода заключается в том, что требования к поведению API идут upstream, от клиента к серверу.

Контракт фокусируется на том поведении, которое действительно важно потребителю. Делает явными его допущения относительно API.

Главная задача CDC — сблизить понимание поведения API его разработчиками и разработчиками его клиентов. Этот подход хорошо сочетается с BDD, на встречах трёх амиго можно набрасывать заготовки для контракта. В конечном счёте, этот контракт так же служит улучшению коммуникаций; разделению общего понимания проблемной области и реализации решения внутри и между командами.

Pact

Рассмотрим применение CDC на примере Pact, одной из его реализации. Допустим, мы делаем web-приложение для участников конференций. В очередной итерации команда разрабатывает отображение расписания выступлений — пока без каких либо историй вроде голосования или заметок, только вывод сетки докладов. Исходники примера лежат здесь.

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

  • В UI будет отображаться список с текстом: Название доклада + Докладчики + Дата и время.
  • Для этого бэкенд должен возвращать данные как в примере ниже.
{
   "talks":[
      {
         "title":"Изготовление качественных шерстяных изделий в условиях невесомости",
         "speakers":[
            {
               "name":"Фубар Базов"
            }
         ],
         "time":"2019-05-27T12:00:00+03:00"
      }
   ]
}

После чего разработчик фронтенда идёт писать код клиента (backend for frontend). Он устанавливает в проекте библиотеку для работы с pact-контрактом:

yarn add --dev @pact-foundation/pact

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

const provider = new Pact({
  // название потребителя и поставщика данных
  consumer: "schedule-consumer",
  provider: "schedule-producer",
  // порт, на котором поднимется заглушка
  port: pactServerPort,
  // сюда pact будет писать отладочную информацию
  log: path.resolve(process.cwd(), "logs", "pact.log"),
  // директория, в которой сформируется контракт
  dir: path.resolve(process.cwd(), "pacts"),
  logLevel: "WARN",
  // версия DSL контракта
  spec: 2
});

Контракт представляет собой JSON-файл, в котором описаны сценарии взаимодействия клиента с сервисом. Но описывать его вручную не нужно, так как он формируется из настроек заглушки в коде. Разработчик перед тестом описывает следующее поведение.

provider.setup().then(() =>
  provider
    .addInteraction({
      uponReceiving: "a request for schedule",
      withRequest: {
        method: "GET",
        path: "/schedule"
      },
      willRespondWith: {
        status: 200,
        headers: {
          "Content-Type": "application/json;charset=UTF-8"
        },
        body: {
          talks: [
            {
              title: "Изготовление качественных шерстяных изделий в условиях невесомости",
              speakers: [
                {
                  name: "Фубар Базов"
                }
              ],
              time: "2019-05-27T12:00:00+03:00"
            }
          ]
        }
      }
    })
    .then(() => done())
);

Здесь в примере мы указали конкретный ожидаемый запрос к сервису, но pact-js также поддерживает несколько способов определения совпадений.

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

it("fetches schedule", done => {
  fetch(`http://localhost:${pactServerPort}/schedule`)
    .then(response => response.json())
    .then(json => expect(json).toStrictEqual({
      talks: [
        {
          title: "Изготовление качественных шерстяных изделий в условиях невесомости",
          speakers: [
            {
              name: "Фубар Базов"
            }
          ],
          time: "2019-05-27T12:00:00+03:00"
        }
      ]
    }))
    .then(() => done());
});

В реальном проекте это может быть как быстрый unit-тест отдельной функции интерпретации ответа, так и медленный UI-тест отображения полученных от сервиса данных.

Во время прогона теста, pact проверяет, что заглушка получила заданный в тестах запрос. Расхождения можно посмотреть в виде diff в файле pact.log.

E, [2019-05-21T01:01:55.810194 #78394] ERROR -- : Diff with interaction: "a request for schedule"
Diff
--------------------------------------
Key: - is expected
     + is actual
Matching keys and values are not shown
 {
   "headers": {
-    "Accept": "application/json"
+    "Accept": "*/*"
   }
 }
Description of differences
--------------------------------------
* Expected "application/json" but got "*/*" at $.headers.Accept

Если тест проходит успешно, то формируется контракт в формате JSON. В нём описано ожидаемое поведение API.

{
  "consumer": {
    "name": "schedule-consumer"
  },
  "provider": {
    "name": "schedule-producer"
  },
  "interactions": [
    {
      "description": "a request for schedule",
      "request": {
        "method": "GET",
        "path": "/schedule",
        "headers": {
          "Accept": "application/json"
        }
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json;charset=UTF-8"
        },
        "body": {
         "talks":[
            {
               "title":"Изготовление качественных шерстяных изделий в условиях невесомости",
               "speakers":[
                  {
                     "name":"Фубар Базов"
                  }
               ],
               "time":"2019-05-27T12:00:00+03:00"
            }
         ]
       }}}
  ],
  "metadata": {
    "pactSpecification": {
      "version": "2.0.0"
    }
  }
}

Он отдаёт этот контракт разработчику бэкенда. Допустим, API будет на Spring Boot. У Pact есть библиотека pact-jvm-provider-spring, которая умеет работать с MockMVC. Но мы расмотрим на Spring Cloud Contract, который реализует CDC в экосистеме Spring. Он использует свой формат контрактов, но также имеет точку расширения для подключения конвертеров из других форматов. Его родной формат контрактов поддерживается только самим Spring Cloud Contract — в отличие от Pact, у которого есть библиотеки для JVM, Ruby, JS, Go, Python и т.д.

Допустим, в нашем примере разработчик бэкенда использует Gradle для сборки сервиса. Он подключает следующие зависимости:

buildscript {
	// ...
	dependencies {
		classpath "org.springframework.cloud:spring-cloud-contract-pact:2.1.1.RELEASE"
	}
}
plugins {
	id "org.springframework.cloud.contract" version "2.1.1.RELEASE"
       // ...
}
// ...
dependencies {
    // ...
    testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-verifier'
}

И кладет полученный от фротэндера Pact-контракт в директорию src/test/resources/contracts.
Из неё по-умолчанию плагин spring-cloud-contract вычитывает контракты. При сборке исполняется gradle-задача generateContractTests, которая формирует в директории build/generated-test-sources следующий тест.

public class ContractVerifierTest extends ContractsBaseTest {
    @Test
    public void validate_aggregator_client_aggregator_service() throws Exception {
        // given:
        MockMvcRequestSpecification request = given()
            .header("Accept", "application/json");
        // when:
        ResponseOptions response = given().spec(request)
            .get("/scheduler");
        // then:
        assertThat(response.statusCode()).isEqualTo(200);
        assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");
        // and:
        DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
        assertThatJson(parsedJson).array("['talks']").array("['speakers']").contains("['name']").isEqualTo( /*...*/ );
        assertThatJson(parsedJson).array("['talks']").contains("['time']").isEqualTo( /*...*/ );
        assertThatJson(parsedJson).array("['talks']").contains("['title']").isEqualTo( /*...*/ );
    }
}

При запуске этого теста мы увидим ошибку:

java.lang.IllegalStateException: You haven't configured a MockMVC instance. You can do this statically

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

public abstract class ContractsBaseTest {
    private ScheduleController scheduleController = new ScheduleController();
    @Before
    public void setup() {
        RestAssuredMockMvc.standaloneSetup(scheduleController);
    }
}

Чтобы этот базовый класс использовался при генерации, нужно донастроить gradle-плагин spring-cloud-contract.

contracts {
	baseClassForTests = 'ru.example.schedule.ContractsBaseTest'
}

Теперь у нас генерируется такой тест:

public class ContractVerifierTest extends ContractsBaseTest {
	@Test
	public void validate_aggregator_client_aggregator_service() throws Exception {
		// ...
	}
}

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

Загрузку нового сгенерированного контракта в брокер можно сделать шагом CI при сборке клиента. А в коде сервера указать динамическую загрузку контракта по URL. Spring Cloud Contract это также поддерживает.

Применимость CDC

Какие ограничения есть у Consumer Driven Contracts?
За использование такого подхода приходится платить дополнительными инструментами вроде pact. Сами по себе контракты — это дополнительный артефакт, ещё одна абстракция, которую нужно аккуратно поддерживать, осознанно применять к ней инженерные практики.

Они не заменяют e2e тесты, так как заглушки всё равно остаются заглушками — моделями реальных компонентов системы, которые может и чуть-чуть, но не соответствуют действительности. Через них не проверить сложные сценарии.
Также CDC не заменяют функциональные тесты API. Они дороже в поддержке, чем Plain Old Unit Tests. Разработчики Pact рекомендуют пользоваться следующей эвристикой — если убрать контракт и это не вызовет ошибки или неправильную интерпретацию со стороны клиента — значит он не нужен. К примеру, не нужно через контракт описывать абсолютно все коды ошибок API, если клиент обрабатывает их одинаково. Другими словами, контракт описывает для сервиса только то, что важно его клиенту. Не больше, но и не меньше.

Слишком большое количество контрактов также усложняет эволюцию API. Каждый дополнительный контракт — это повод для красных тестов. Нужно проектировать CDC таким образом, чтобы каждый fail теста нес в себе полезную смысловую нагрузку, которая перевешивает стоимость его поддержки. К примеру, если в контракте зафиксировать минимальную длину некоторого текстового поля, которая безразлична потребителю (он применяет технику Toleran Reader), то каждое изменение этого минимального значения будет ломать контракт и нервы окружающих. Такую проверку нужно переносить на уровень самого API и реализовывать в зависимости от источника ограничений.

Заключение

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

В то же время, инструменты и фреймворки CDC активно разрабатываются и уже достигли зрелости для пробы на ваших проектах. Тестируйте 🙂

На конференции QualityConf 27-28 мая Андрей Маркелов расскажет про техники тестирования на проде, а Артур Хинельцев — про мониторинг высоконагруженного фронтенда, когда цена даже маленькой ошибки — это десятки тысяч грустных пользователей.

Приходите пообщаться за качество!

Source: habr1

Метки:





Запись опубликована: 22.05.2019

[Из песочницы] Arduino и Raspberry вне закона

Эта публикация призвана обратить внимание сообщества радиолюбителей и небольших производителей РЭА на грядущее изменение законодательства. Возможно, в ближайшем будущем за изготовление и продажу умной кормушки для котика на Arduino без декларации о соответствии вы будете преследоваться по закону, а затем наказаны по всей его строгости.

Начну с того, что производители низковольтного оборудования на территории РФ обязаны соблюдать, как минимум, Технический регламент таможенного союза «О безопасности низковольтного оборудования» (ТР ТС 004/2011). Под понятием «низковольтное оборудование» до настоящего момента понимается электрическое оборудование, предназначенное для использования при номинальном напряжении от 50 до 1000 В (включительно) переменного тока и от 75 до 1500 В (включительно) постоянного тока. Сюда же относятся компьютеры, ручной электроинструмент, бытовые приборы и так далее. Однако под это определение не попадает электроника с номинальным напряжением ниже 50 и 75 Вольт для AC и DC соответственно. То есть далеко не вся электроника попадает в эти рамки. Например, Arduino с Raspberry и вся их экосистема не попадает.

Но законодателя такое положение дел почему-то не устраивает, и вот уже готовится к подписанию Решение о внесении изменений №1 в вышеназванный Технический регламент. Одно из главных изменений — критерий применимости технического регламента. Теперь под понятие «низковольтное оборудование» попадает все, что использует напряжение от 0 до 1000 В переменного и от 0 до 1500 В постоянного. То есть абсолютно вся электроника.

Что с того, что любая электроника подпадает под действие Технического регламента?

Вот перечень лишь самых ощутимых последствий:

  • Необходимость подтверждать соответствие Техническому регламенту как минимум посредством Декларации о соответствии.
  • Необходимость проводить испытания продукции в аккредитованной лаборатории.
  • Запрет на продажу продукции даже в единичном экземпляре без Декларации.

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

Теги:

Похожие публикации

Source: habr1

Метки:





Запись опубликована: 22.05.2019

ML на Scala с улыбкой, для тех, кто не боится экспериментов



Юла

61,00

Сlassified 2.0

Всем привет! Сегодня будем говорить о реализации машинного обучения на Scala. Начну с объяснения, как мы докатились до такой жизни. Итак, наша команда долгое время использовала все возможности машинного обучения на Python. Это удобно, есть много полезных библиотек для подготовки данных, хорошая инфраструктура для разработки, я имею в виду Jupyter Notebook. Всё бы ничего, но столкнулись с проблемой распараллеливания вычислений в production, и решили использовать в проде Scala. Почему бы и нет, подумали мы, там есть куча библиотек, даже Apache Spark написан на Scala! При этом, сегодня модели мы разрабатываем на Python, а затем повторяем обучение на Scala для дальнейшей сериализации и использования в production. Но, как говорится, дьявол кроется в деталях.

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

На практике оказалось не так уж всё и радостно: не так много библиотек, реализующих классические алгоритмы машинного обучения, а те, что есть — это, зачастую, OpenSource-проекты без поддержки крупных вендоров. Да, безусловно, есть Spark MLib, но он сильно привязан к экосистеме Apache Hadoop, да и тащить его в микросервисную архитектуру уж очень не хотелось.

Нужно было решение, которое спасёт мир и вернёт спокойный сон, и оно было найдено!

Что нужно?

Когда мы выбирали инструмент для машинного обучения, то исходили из таких критериев:

  • он должен быть простой;
  • несмотря на простоту, широкую функциональность никто не отменял;
  • очень хотелось иметь возможность разрабатывать модели в web-интерпретаторе, а не через консоль или постоянные сборки и компиляции;
  • наличие документации играет важную роль;
  • в идеале, чтобы была поддержка, хотя бы отвечающая на github issues.

Что мы посмотрели?

  • Apache Spark MLib: нам не подошёл. Как было сказано выше, этот набор библиотек сильно привязан к стеку Apache Hadoop и самому Spark Core, который слишком много весит, чтобы строить микросервисы на его основе.
  • Apache PredictionIO: интересный проект, много контрибьюторов, есть документация с примерами. По сути, это REST-сервер, на котором крутятся модели. Есть готовые модели, например, классификация текста, запуск которых описан в документации. В документации описано, как можно добавлять и обучать свои модели. Нам не подошёл, так как под капотом используется Spark, и это больше из области монолитного решения, а не микросервисная архитектура.
  • Apache MXNet: интересный фреймворк для работы с нейронными сетями, есть поддержка Scala и Python — это удобно, можно обучать нейронную сеть на Python, а потом сохранённый результат подгружать из Scala при создании production-решения. Мы его используем в production-решениях, об этом есть отдельная статья тут.
  • Smile: очень похож на пакет scikit-learn для Python. Есть много реализаций классических алгоритмов машинного обучения, хорошая документация с примерами, поддержка на github, встроенный визуализатор (работает на базе Swing), для разработки моделей можно использовать Jupyter Notebook. Это как раз то, что нужно!

Подготовка окружения

Итак, мы выбрали Smile. Расскажу, как запустить его в Jupyter Notebook на примере алгоритма кластеризации k-means. Первое, что нам нужно сделать — установить Jupyter Notebook с поддержкой Scala. Это можно сделать через pip, или использовать уже собранный и настроенный Docker-образ. Я за более простой, второй вариант.

Чтобы подружить Jupyter со Scala, я хотел воспользоваться BeakerX, входящим в состав Docker-образа, доступного в официальном репозитории BeakerX. Этот образ рекомендован в документации Smile, и запустить его можно так:

# Официальный образ BeakerX
docker run -p 8888:8888 beakerx/beakerx

Но здесь поджидала первая неприятность: на момент написания статьи внутри образа beakerx/beakerx был установлен BeakerX 1.0.0, а в официальном github проекта уже доступна версия 1.4.1 (точнее, последний релиз 1.3.0, но в мастере лежит 1.4.1, и она работает 🙂 ).

Понятное дело, что хочется работать с последней версией, поэтому я собрал собственный образ на основе BeakerX 1.4.1. Не буду утомлять вас содержанием Dockerfile, вот ссылка на него.

# Запускаем образ и монтируем в него рабочую директорию
mkdir -p /tmp/my_code
docker run -it 
    -p 8888:8888 
    -v /tmp/my_code:/workspace/my_code 
    entony/jupyter-scala:1.4.1

Кстати, для тех, кто будет использовать мой образ, будет небольшой бонус: в директории examples есть пример k-means для случайной последовательности с построением графика (это не совсем тривиальная задача для Scala notebooks).

Загрузка Smile в Jupyter Notebook

Отлично, окружение подготовили! Создаём в папке в нашей директории новый Scala notebooks, далее необходимо выкачать из Maven библиотеки для работы Smile.

%%classpath add mvn
com.github.haifengl smile-scala_2.12 1.5.2

После исполнения кода в его блоке вывода появится список загруженных jar-файлов.

Следующий шаг: импортирование необходимых пакетов для работы примера.

import java.awt.image.BufferedImage
import java.awt.Color
import javax.imageio.ImageIO
import java.io.File
import smile.clustering._

Подготовка данных для кластеризации

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

// Размер изображения будет 640 х 360
val width = 640
val hight = 360
// Создаём пустое изображение нужного размера
val testImage = new BufferedImage(width, hight, BufferedImage.TYPE_INT_RGB)
// Заполняем изображение пикселями. Преобладающим будет синий цвет.
for {
    x <- (0 until width)
    y <- (0 until hight)
    color = if (y <= hight / 3 && (x <= width / 3 || x > width / 3 * 2)) Color.RED
    else if (y > hight / 3 * 2 && (x <= width / 3 || x > width / 3 * 2)) Color.GREEN
    else Color.BLUE
} testImage.setRGB(x, y, color.getRGB)
// Выводим созданное изображение
testImage

В результате выполнения этого кода выводится вот такая картинка:


Следующий шаг: преобразуем картинку в набор пикселей. Под пикселем будем понимать сущность с такими свойствами:

  • координата по широкой стороне (x);
  • координата по узкой стороне (y);
  • значение цвета;
  • опциональное значение класса/номера кластера (до выполнения кластеризации будет пустым).

В качестве сущности удобно использовать case class:

case class Pixel(x: Int, y: Int, rgbArray: Array[Double], clusterNumber: Option[Int] = None)

Здесь для значений цвета используется массив rgbArray из трёх значений красного, зелёного и синего (например, для красного цвета Array(255.0, 0, 0)).

// Перегоняем изображение в коллекцию пикселей (Pixel)
val pixels = for {
    x <- (0 until testImage.getWidth).toArray
    y <- (0 until testImage.getHeight)
    color = new Color(testImage.getRGB(x, y))
} yield Pixel(x, y, Array(color.getRed.toDouble, color.getGreen.toDouble, color.getBlue.toDouble))
// Выводим первый 10 элементов коллекции
pixels.take(10)

На этом подготовка данных закончена.

Кластеризация пикселей по цветам

Итак, у нас есть коллекция из пикселей трёх основных цветов, поэтому кластеризовать пиксели мы будем на три класса.

// Количество кластеров
val countColors = 3
// Выполняем кластеризацию
val clusters = kmeans(pixels.map(_.rgbArray), k = countColors, runs = 20)

В документации рекомендуется задавать параметр runs в диапазоне от 10 до 20.

При выполнении этого кода будет создан объект типа KMeans. В блоке вывода будет информация о результатах кластеризации:

K-Means distortion: 0.00000
Clusters of 230400 data points of dimension 3:
  0    50813 (22.1%)
  1    51667 (22.4%)
  2    127920 (55.5%)

Один из кластеров действительно содержит больше пикселей, чем остальные. Теперь нужно разметить нашу коллекцию пикселей классами от 0 до 2.

// Разметка коллекции пикселей
val clusteredPixels = (pixels zip clusters.getClusterLabel()).map {case (pixel, cluster) => pixel.copy(clusterNumber = Some(cluster))}
// Выводим 10 размеченных пикселей
clusteredPixels.take(10)

Перекрашиваем изображение

Осталось дело за малым — выделить кластер с наибольшим количеством пикселей и перекрасить все пиксели, входящие в этот кластер, в серый цвет (изменить значение массива rgbArray).

// Серый цвет
val grayColor = Array(127.0, 127.0, 127.0)
// Определяем кластер с наибольшим количеством пикселей
val blueClusterNumber = clusteredPixels.groupBy(pixel => pixel.clusterNumber)
    .map {case (clusterNumber, pixels) => (clusterNumber, pixels.size) }
    .maxBy(_._2)._1
// Перекрашиваем все пиксели кластера в серый
val modifiedPixels = clusteredPixels.map {
    case p: Pixel if p.clusterNumber == blueClusterNumber => p.copy(rgbArray = grayColor)
    case p: Pixel => p
}
// Выводим 10 элементов из новой коллекции пикселей
modifiedPixels.take(10)

Тут нет ничего сложного, просто группируем по номеру кластера (это у нас Option:[Int]), считаем количество элементов в каждой группе и вытаскиваем кластер с максимальным количеством элементов. Далее меняем цвет на серый только у тех пикселей, которые относятся к найденному кластеру.

Создаём новое изображение и сохраняем результаты

Собираем из коллекции пикселей новое изображение:

// Создаём пустое изображение такого же размера
val modifiedImage = new BufferedImage(testImageWidth, testImageHight, BufferedImage.TYPE_INT_RGB)
// Наполняем его перекрашенными пикселями
modifiedPixels.foreach {
    case Pixel(x, y, rgbArray, _) =>
        val r = rgbArray(0).toInt
        val g = rgbArray(1).toInt
        val b = rgbArray(2).toInt
        modifiedImage.setRGB(x, y, new Color(r, g, b).getRGB)
}
// Выводим новое изображение
modifiedImage

Вот что, в итоге, у нас получилось.



Сохраняем оба изображения.

ImageIO.write(testImage, "png", new File("testImage.png"))
ImageIO.write(modifiedImage, "png", new File("modifiedImage.png"))

Заключение

Машинное обучение на Scala существует. Для реализации базовых алгоритмов не обязательно тащить какую-то огромную библиотеку. Представленный выше пример показывает, что при разработке можно не отказываться от привычных средств, тот же Jupyter Notebook можно, без особого труда, подружить со Scala.

Конечно же, для полного обзора всех возможностей Smile не хватит одной статьи, да это и не входило в планы. Основную задачу — приоткрыть дверь в мир машинного обучения на Scala — считаю выполненной. Пользоваться ли этими инструментами, и, уж тем более, тащить их в production или нет, решать вам!

Ссылки

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Вам будет интересно прочитать про примеры других алгоритмов, реализованных на Scala Smile?

Проголосовали 8 пользователей. Воздержались 3 пользователя.

Source: habr1

Метки:





Запись опубликована: 22.05.2019

JMAP — открытый протокол заменит IMAP при обмене электронными письмами

В начале месяца на Hacker News активно обсуждался протокол JMAP, разрабатываемый под руководством IETF. Мы решили поговорить о том, зачем он понадобился и как устроен.


/ PxHere / PD

Чем не угодил IMAP

Протокол IMAP был представлен в 1986 году. Многие вещи, описанные в стандарте, на сегодняшний день уже не актуальны. Например, протокол умеет возвращать количество строк письма и контрольные суммы MD5 — эта функциональность в современных почтовых клиентах практически не используется.

Другая проблема связана с потреблением трафика. В случае с IMAP электронные письма хранятся на сервере и периодически синхронизируются с локальными клиентами. Если по какой-то причине копия на устройстве пользователя «портится», приходится заново синхронизировать всю почту. В современном мире, когда к серверу могут быть подключены тысячи мобильных устройств, такой подход приводит к повышенному расходу трафика и вычислительных ресурсов.

Сложности возникают не только с самим протоколом, но и почтовыми клиентами, которые с ним работают. С момента создания IMAP многократно подвергался различным ревизиям — актуальной версией на сегодняшний день является IMAP4. При этом для него существует множество опциональных расширений — в сети опубликованы девяносто RFC с дополнениями. Одним из самых свежих является RFC8514, представленный в 2019 году.

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

«Более того, современный почтовый клиент должен не просто пересылать сообщения, но уметь работать с контактами и синхронизироваться с календарем, — говорит Сергей Белкин, начальник отдела развития IaaS-провайдера 1cloud.ru. — Сегодня для этих целей используют сторонние протоколы вроде LDAP, CardDAV и CalDAV. Такой подход усложняет настройку файрволов в корпоративных сетях и открывает новые векторы для кибератак».

Решить обозначенные проблемы призван JMAP. Его разрабатывают специалисты из FastMail под руководством Инженерного совета интернета (IETF). Протокол работает поверх HTTPS, использует JSON (по этой причине он подойдет не только для обмена электронными сообщениями, но и для решения ряда задач в облаке) и упрощает организацию работы с почтой в мобильных системах. Кроме обработки писем, в JMAP также предусмотрена возможность подключения расширений для работы с контактами и календарём-планировщиком.

Особенности нового протокола

JMAP является протоколом без сохранения состояния (stateless) и не требует постоянного соединения с почтовым сервером. Эта особенность упрощает работу в нестабильных мобильных сетях и позволяет экономить заряд батареи устройств.

Электронное письмо в JMAP представлено в формате JSON-структуры. Она содержит всю информацию из сообщения RFC5322 (Internet Message Format), которая может понадобиться почтовым приложениям. По словам разработчиков, такой подход должен упростить создание клиентов, так как за решение потенциальных сложностей (связанных с MIME, чтением заголовков и кодированием) будет отвечать сервер.

Для обращения к серверу клиент использует API. Для этого он формирует аутентифицированный POST-запрос, свойства которого описаны в сессионном объекте JMAP. Запрос имеет формат application/json и состоит из одного объекта запроса JSON. Сервер также генерирует один объект ответа.

В спецификации (пункт 3) авторы приводят следующий пример с запросом:

{
  "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
  "methodCalls": [
    [ "method1", {
      "arg1": "arg1data",
      "arg2": "arg2data"
    }, "c1" ],
    [ "method2", {
      "arg1": "arg1data"
    }, "c2" ],
    [ "method3", {}, "c3" ]
  ]
}

Ниже — пример ответа, который сформирует сервер:

{
  "methodResponses": [
    [ "method1", {
      "arg1": 3,
      "arg2": "foo"
    }, "c1" ],
    [ "method2", {
      "isBlah": true
    }, "c2" ],
    [ "anotherResponseFromMethod2", {
      "data": 10,
      "yetmoredata": "Hello"
    }, "c2"],
    [ "error", {
      "type":"unknownMethod"
    }, "c3" ]
  ],
  "sessionState": "75128aab4b1b"
}

Полную спецификацию JMAP с примерами реализации можно найти на официальном сайте проекта. Там же авторы разместили описание спецификаций для JMAP Contacts и JMAP Calendars — они направлены на работу с календарями и списками контактов. По словам авторов, Contacts и Calendars выделили в отдельные документы, чтобы их можно было дорабатывать и стандартизировать независимо от «ядра». Исходные коды для JMAP — в репозитории на GitHub.


/ PxHere / PD

Перспективы

Несмотря на то что официально работа над стандартом еще не завершена, его уже внедряют в продакшн-средах. Например, создатели открытого почтового сервера Cyrus IMAP реализовали его JMAP-версию. Разработчики из FastMail выпустили серверный фреймворк для нового протокола на Perl, а авторы JMAP представили прокси-сервер.

Можно ожидать, что в будущем проектов на базе JMAP будет становиться все больше. Например, есть некоторая вероятность, что на новый протокол перейдут разработчики из Open-Xchange, создающие IMAP-сервер для Linux-систем. Отказаться от IMAP их очень просят участники комьюнити, сформировавшегося вокруг инструментов компании.

Разработчики из IETF и FastMail говорят, что все больше пользователей видят необходимость в новом открытом стандарте для обмена сообщениями. Авторы JMAP надеются, что в будущем больше компаний начнет внедрять у себя этот протокол.

Как проверить cookies на соответствие GDPR — поможет новый открытый инструмент

Как сэкономить с помощью прикладного программного интерфейса
DevOps в облачном сервисе на примере 1cloud.ru
Эволюция архитектуры облака 1cloud

Потенциальные атаки на HTTPS и способы защиты от них
Как защитить сервер в интернете: опыт 1cloud.ru
Короткий ликбез: что такое Continuous Integration

Source: habr1

Метки:





Запись опубликована: 22.05.2019

[Перевод] JavaScript-движки: как они работают? От стека вызовов до промисов — (почти) всё, что вам нужно знать

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

Откройте консоль в Chrome и перейдите на вкладку Sources. Вы увидите несколько разделов, и один из самых интересных называется Call Stack (в Firefox вы увидите Call Stack, когда поставите брейкпоинт в коде):

Что такое Call Stack? Похоже, тут много чего происходит, даже ради исполнения пары строк кода. На самом деле JavaScript не поставляется в коробке с каждым браузером. Существует большой компонент, который компилирует и интерпретирует наш JavaScript-код — это JavaScript-движок. Самыми популярными являются V8, он используется в Google Chrome и Node.js, SpiderMonkey в Firefox, JavaScriptCore в Safari/WebKit.

Сегодня JavaScript-движки представляют собой прекрасные образцы программной инженерии, и будет практически невозможно рассказать обо всех аспектах. Однако основную работу по исполнению кода делают для нас лишь несколько компонентов движков: Call Stack (стек вызовов), Global Memory (глобальная память) и Execution Context (контекст исполнения). Готовы с ними познакомиться?

Содержание:

  1. JavaScript-движки и глобальная память
  2. JavaScript-движки: как они работают? Глобальный контекст исполнения и стек вызовов
  3. JavaScript является однопоточным, и другие забавные истории
  4. Асинхронный JavaScript, очередь обратных вызовов и цикл событий
  5. Callback hell и промисы ES6
  6. Создание и работа с JavaScript-промисами
  7. Обработка ошибок в ES6-промисах
  8. Комбинаторы ES6-промисов: Promise.all, Promise.allSettled, Promise.any и другие
  9. ES6-промисы и очередь микрозадач
  10. JavaScript-движки: как они работают? Асинхронная эволюция: от промисов до async/await
  11. JavaScript-движки: как они работают? Итоги

1. JavaScript-движки и глобальная память

Я говорил, что JavaScript является одновременно компилируемым и интерпретируемым языком. Хотите верьте, хотите нет, но на самом деле JavaScript-движки компилируют ваш код за микросекунды до его исполнения.

Волшебство какое-то, да? Это волшебство называется JIT (Just in time compilation). Она сама по себе является большой темой для обсуждения, даже книги будет мало, чтобы описать работу JIT. Но пока что мы пропустим теорию и сосредоточимся на фазе исполнения, которая не менее интересна.

Для начала посмотрите на этот код:

var num = 2;
function pow(num) {
    return num * num;
}

Допустим, я спрошу вас, как этот код обрабатывается в браузере? Что вы ответите? Вы можете сказать: «браузер читает код» или «браузер исполняет код». В реальности всё не так просто. Во-первых, код считывает не браузер, а движок. JavaScript-движок считывает код, и как только он определяет первую строку, то кладёт пару ссылок в глобальную память.

Глобальная память (которую также называют кучей (heap)) — это область, в которой JavaScript-движок хранит переменные и объявления функций. И когда он прочитает приведённый выше код, то в глобальной памяти появятся два биндинга:

Даже если в примере содержится лишь переменная и функция, представьте, что ваш JavaScript-код исполняется в более крупной среде: в браузере или в Node.js. В таких средах есть много заранее определённых функций и переменных, которые называют глобальными. Поэтому глобальная память будет содержать гораздо больше данных, чем просто num и pow, имейте в виду.

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

var num = 2;
function pow(num) {
    return num * num;
}
pow(num);

Что произойдёт? А произойдёт кое-что интересное. При вызове функции JavaScript-движок выделит два раздела:

  • Глобальный контекст исполнения (Global Execution Context)
  • Стек вызовов (Call Stack)

Что они собой представляют?

2. JavaScript-движки: как они работают? Глобальный контекст исполнения и стек вызовов

Вы узнали, как JavaScript-движок читает переменные и объявления функций. Они попадают в глобальную память (кучу).

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

Это стековая структура данных: элементы могут добавляться в неё сверху, но они не могут исключаться из структуры, пока над ними есть другие элементы. Именно так устроены JavaScript-функции. При исполнении они не могут покинуть стек вызовов, если в нём присутствует другая функция. Обратите на это внимание, поскольку эта концепция помогает понять утверждение «JavaScript является однопоточным».

Но вернёмся к нашему примеру. При вызове функции движок отправляет её в стек вызовов:

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

В то же самое время движок размещает в памяти глобальный контекст исполнения, это глобальная среда, в которой исполняется JavaScript-код. Вот как это выглядит:

Представьте глобальный контекст исполнения в виде моря, в котором глобальные JavaScript-функции плавают, словно рыбы. Как мило! Но это лишь половина всей истории. Что, если наша функция имеет вложенные переменные, или внутренние функции?

Даже в простом случае, как показано ниже, JavaScript-движок создаёт локальный контекст исполнения:

var num = 2;
function pow(num) {
    var fixed = 89;
    return num * num;
}
pow(num);

Обратите внимание, что я добавил в функцию pow переменную fixed. В этом случае локальный контекст исполнения будет содержать раздел для fixed. Я не очень хорошо рисую маленькие прямоугольники внутри других маленьких маленьких прямоугольников, так что используйте своё воображение.

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

Давайте теперь вернёмся к истории с однопоточностью. Что это означает?

3. JavaScript является однопоточным, и другие забавные истории

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

Это не проблема, если мы работаем с синхронным кодом. К примеру, сложение двух чисел является синхронным и вычисляется за микросекунды. А что насчёт сетевых вызовов и других взаимодействий с внешним миром?

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

В то же время вы знаете, что когда браузер загружает какой-то JavaScript-код, движок считывает этот код строка за строкой и выполняет следующие шаги:

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

Теперь у вас есть общее представление о механике синхронности, лежащей в основе всех JavaScript-движков. В следующей главе мы поговорим о том, как в JavaScript работает асинхронный код и почему он работает именно так.

4. Асинхронный JavaScript, очередь обратных вызовов и цикл событий

Благодаря глобальной памяти, контексту исполнения и стеку вызовов синхронный JavaScript-код исполняется в наших браузерах. Но мы кое о чём забыли. Что происходит, если нужно исполнить какую-нибудь асинхронную функцию?

Под асинхронной функцией я подразумеваю каждое взаимодействие с внешним миром, для завершения которого может потребоваться какое-то время. Вызов REST API или таймера — асинхронны, потому что на их выполнение могут уйти секунды. Благодаря имеющимся в движке элементам мы можем обрабатывать такие функции без блокирования стека вызовов и браузера. Не забывайте, стек вызовов может исполнять одновременно только одну функцию, и даже одна блокирующая функция может буквально остановить браузер. К счастью, JavaScript-движки «умны», и с небольшой помощью браузера могут такие вещи отсортировывать.

Когда мы исполняем асинхронную функцию, браузер берёт её и выполняет для нас. Возьмём такой таймер:

setTimeout(callback, 10000);
function callback(){
    console.log('hello timer!');
}

Уверен, что хоть вы и видели setTimeout уже сотни раз, однако можете не знать, что эта функция не встроена в JavaScript. Вот так, когда JavaScript появился, в нём не было функции setTimeout. По сути, она является частью так называемых браузерных API, коллекции удобных инструментов, которые нам предоставляет браузер. Чудесно! Но что это означает на практике? Поскольку setTimeout относится к браузерным API, эта функция исполняется самим браузером (на мгновение она появляется в стеке вызовов, но сразу оттуда удаляется).

Через 10 секунд браузер берёт callback-функцию, которую мы ему передали, и кладёт её в очередь обратных вызовов. В данный момент в JavaScript-движке появилось ещё два раздела-прямоугольника. Посмотрите на этот код:

var num = 2;
function pow(num) {
    return num * num;
}
pow(num);
setTimeout(callback, 10000);
function callback(){
    console.log('hello timer!');
}

Теперь наша схема выглядит так:

setTimeout исполняется внутри контекста браузера. Через 10 секунд таймер запускается и callback-функция готова к исполнению. Но для начала она должна пройти через очередь обратных вызовов. Это структура данных в виде очереди, и, как свидетельствует её название, представляет собой упорядоченную очередь из функций.

Каждая асинхронная функция должна пройти через очередь обратных вызовов, прежде чем попасть в стек вызовов. Но кто отправляет функции дальше? Это делает компонент под названием цикл событий.

Пока что цикл событий занимается только одним: проверяет, пуст ли стек вызовов. Если в очереди обратных вызовов есть какая-нибудь функция и если стек вызовов свободен, тогда пора отправлять callback в стек вызовов.

После этого функция считается исполненной. Так выглядит общая схема обработки асинхронного и синхронного кода JavaScript-движком:

Допустим, callback() готова к исполнению. После завершения исполнения pow() стек вызовов освобождается и цикл событий отправляет в него callback(). И всё! Хотя я немного всё упростил, если вы поняли приведённую выше схему, то можете понять и весь JavaScript.

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

И если интересно, можете посмотреть любопытное видео «What the heck is the event loop anyway» Филипа Робертса. Это одно из лучших объяснений цикла событий.

Но мы ещё не закончили с темой асинхронного JavaScript. В следующих главах мы рассмотрим ES6-промисы.

5. Callback hell и ES6-промисы

Callback-функции используются в JavaScript везде, и в синхронном, и в асинхронном коде. Рассмотрим этот метод:

function mapper(element){
    return element * 2;
}
[1, 2, 3, 4, 5].map(mapper);

mapper — это callback-функция, которая передаётся внутри map. Приведённый код является синхронным. А теперь рассмотрим этот интервал:

function runMeEvery(){
    console.log('Ran!');
}
setInterval(runMeEvery, 5000);

Этот код асинхронный, поскольку внутри setInterval мы передаём обратный вызов runMeEvery. Обратные вызовы применяются по всему JavaScript, так что у нас годами существует проблема, получившая название «callback hell» — «ад обратных вызовов».

Термин Callback hell в JavaScript применяют к «стилю» программирования, при котором callback’и вкладывают в другие callback’и, которые вложены в другие callback’и… Из-за асинхронной природы JavaScript-программисты уже давно попадают в эту ловушку.

Если честно, я никогда не создавал большие пирамиды callback’ов. Возможно, потому что я ценю читабельный код и всегда стараюсь придерживаться его принципов. Если вы попали в callback hell, это говорит о том, что ваша функция делает слишком много.

Я не будут подробно говорить о callback hell, если вам интересно, то сходите на сайт callbackhell.com, там эта проблема подробно исследована и предложены разные решения. А мы поговорим о ES6-промисах. Это аддон к JavaScript, призванное решить проблему ада обратных вызовов. Но что такое «промисы»?

Промис в JavaScript — это представление будущего события. Промис может завершиться успешно, или на жаргоне программистов промис будет «разрешён» (resolved, исполнен). Но если промис завершается с ошибкой, то мы говорим, что он в состоянии «отклонён» (rejected). Также у промисов есть состояние по умолчанию: каждый новый промис начинается в состоянии «ожидания решения» (pending). Можно ли создать собственный промис? Да. Об этом мы поговорим в следующей главе.

6. Создание и работа с JavaScript-промисами

Для создания нового промиса нужно вызвать конструктор, передав в него callback-функцию. Она может принимать только два параметра: resolve и reject. Давайте создадим новый промис, который будет разрешён через 5 секунд (можете протестировать примеры в браузерной консоли):

const myPromise = new Promise(function(resolve){
    setTimeout(function(){
        resolve()
    }, 5000)
});

Как видите, resolve — это функция, которую мы вызываем, чтобы промис успешно завершился. А reject создаст отклонённый промис:

const myPromise = new Promise(function(resolve, reject){
    setTimeout(function(){
        reject()
    }, 5000)
});

Обратите внимание, что вы можете игнорировать reject, потому что это второй параметр. Но если вы намерены воспользоваться reject, то не сможете проигнорировать resolve. То есть следующий код не будет работать и закончится разрешённым промисом:

// Can't omit resolve !
const myPromise = new Promise(function(reject){
    setTimeout(function(){
        reject()
    }, 5000)
});

Сейчас промисы не выглядят такими полезными, верно? Эти примеры ничего не выводят для пользователя. Давайте кое-что добавим. И разрешённые, от отклонённые промисы могут возвращать данные. Например:

const myPromise = new Promise(function(resolve) {
  resolve([{ name: "Chris" }]);
});

Но мы всё ещё ничего не видим. Для извлечения данных из промиса вам нужно связать промис с методом then. Он берёт callback (какая ирония!), который получает актуальные данные:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then(function(data) {
    console.log(data);
});

Как JavaScript-разработчик и потребитель чужого кода вы по большей части взаимодействуете с внешними промисами. Создатели библиотек чаще всего обёртывают legacy-код в конструктор промисов, таким образом:

const shinyNewUtil = new Promise(function(resolve, reject) {
  // do stuff and resolve
  // or reject
});

И при необходимости мы также можем создать и разрешить промис, вызвав Promise.resolve():

Promise.resolve({ msg: 'Resolve!'})
.then(msg => console.log(msg));

Итак, напомню: промисы в JavaScript — это закладка на событие, которое произойдёт в будущем. Событие начинается в состоянии «ожидание решения», и может быть успешным (разрешённым, исполненным) или неуспешным (отклонённым). Промис может возвращать данные, которые можно извлечь, прикрепив к промису then. В следующей главе мы обсудим, как работать с ошибками, приходящими из промисов.

7. Обработка ошибок в ES6-промисах

Обрабатывать ошибки в JavaScript всегда было просто, как минимум в синхронном коде. Взгляните на пример:

function makeAnError() {
  throw Error("Sorry mate!");
}
try {
  makeAnError();
} catch (error) {
  console.log("Catching the error! " + error);
}

Результатом будет:

Catching the error! Error: Sorry mate!

Как и ожидалась, ошибка попала в блок catch. Теперь попробуем асинхронную функцию:

function makeAnError() {
  throw Error("Sorry mate!");
}
try {
  setTimeout(makeAnError, 5000);
} catch (error) {
  console.log("Catching the error! " + error);
}

Этот код является асинхронным из-за setTimeout. Что будет, если мы его исполним?

 throw Error("Sorry mate!");
  ^
Error: Sorry mate!
    at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)

Теперь результат другой. Ошибка не была поймана блоком catch, а свободно поднялась выше по стеку. Причина в том, что try/catch работает только с синхронным кодом. Если хотите узнать больше, то эта проблема подробно рассмотрена здесь.

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

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry!');
});

В этом случае мы можем обрабатывать ошибки с помощью обработчика catch, дёргая (опять) обратный вызов:

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry!');
});
myPromise.catch(err => console.log(err));

Кроме того, чтобы для создания и отклонения промиса в нужном месте можно вызывать Promise.reject():

Promise.reject({msg: 'Rejected!'}).catch(err => console.log(err));

Напомню: обработчик then исполняется, когда промис выполнен, а обработчик catch выполняется для отклонённых промисов. Но это ещё не конец истории. Ниже мы увидим, как async/await замечательно работают с try/catch.

8. Комбинаторы ES6-промисов: Promise.all, Promise.allSettled, Promise.any и другие

Промисы не предназначены для работы по одиночке. Promise API предлагает ряд методов для комбинирования промисов. Один из самых полезных — Promise.all, он берёт массив из промисов и возвращает один промис. Только проблема в том, что Promise.all отклоняется, если отклонены все промисы в массиве.

Promise.race разрешает или отклоняет, как только один из промисов в массиве получает соответствующий статус.

В более свежих версиях V8 также будут внедрены два новых комбинатора: Promise.allSettled и Promise.any. Promise.any пока на ранней стадии предложенной функциональности, на момент написания статьи не поддерживается. Однако, в теории, он сможет сигнализировать, был ли исполнен какой-либо промис. Отличие от Promise.race в том, что Promise.any не отклоняется, даже если отклонён один из промисов.

Promise.allSettled ещё интереснее. Он тоже берёт массив промисов, но не «коротит», если один из промисов отклоняется. Он полезен, когда нужно проверить, все ли промисы в массиве перешли в какую-то стадию, вне зависимости от наличия отклонённых промисов. Его можно считать противоположностью Promise.all.

9. ES6-промисы и очередь микрозадач

Если помните из предыдущей главы, каждая асинхронная callback-функция в JavaScript оказывается в очереди обратных вызовов, прежде чем попадает в стек вызовов. Но у callback-функций, переданных в промис, иная судьба: они обрабатываются очередью микрозадач (Microtask Queue), а не очередью задач.

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

Подробнее эта механика описана Джейком Арчибальдом в Tasks, microtasks, queues and schedules, замечательное чтиво.

10. JavaScript-движки: как они работают? Асинхронная эволюция: от промисов до async/await

JavaScript быстро развивается и мы каждый год получаем постоянные улучшения. Промисы выглядели как финал, но с ECMAScript 2017 (ES8) появился новый синтаксис: async/await.

async/await — всего лишь стилистическое улучшение, которое мы называем синтаксическим сахаром. async/await никак не меняет JavaScript (не забывайте, язык должен быть обратно совместим со старыми браузерами и не должен ломать существующий код). Это лишь новый способ написания асинхронного кода на основе премисов. Рассмотрим пример. Выше мы уже сохранили проми с соответствующим then:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
myPromise.then((data) => console.log(data))

Теперь с помощью async/await мы можем обработать асинхронный код так, чтобы для читающего наш листинг код выглядел синхронным. Вместо применения then мы можем обернуть промис в функцию, помеченную как async, и затем будем ожидать (await) результат:

const myPromise = new Promise(function(resolve, reject) {
  resolve([{ name: "Chris" }]);
});
async function getData() {
  const data = await myPromise;
  console.log(data);
}
getData();

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

async function getData() {
  const data = await myPromise;
  return data;
}
getData().then(data => console.log(data));

А что насчёт ошибок? Одно из преимуществ async/await в том, что эта конструкция может позволить нам воспользоваться try/catch. Почитайте введение в обработку ошибок в async-функциях и их тестирование.

Давайте снова взглянем на промис, в котором мы обрабатываем ошибки с помощью обработчика catch:

const myPromise = new Promise(function(resolve, reject) {
  reject('Errored, sorry!');
});
myPromise.catch(err => console.log(err));

С асинхронными функциями мы можем отрефакторить вот так:

async function getData() {
  try {
    const data = await myPromise;
    console.log(data);
    // or return the data with return data
  } catch (error) {
    console.log(error);
  }
}
getData();

Однако ещё не все перешли на этот стиль. try/catch может усложнить ваш код. При этом нужно учитывать ещё кое-что. Посмотрите, как в этом коде возникает ошибка внутри блока try:

async function getData() {
  try {
    if (true) {
      throw Error("Catch me if you can");
    }
  } catch (err) {
    console.log(err.message);
  }
}
getData()
  .then(() => console.log("I will run no matter what!"))
  .catch(() => console.log("Catching err"));

Что насчёт двух строк, которые выводятся в консоли? Не забывайте, что try/catch — синхронная конструкция, а наша асинхронная функция генерирует промис. Они идут по двум разным путям, словно поезда. Но они никогда не встретятся! Поэтому ошибка, которую подняла throw, никогда не активирует обработчик catch в getData(). Исполнение этого кода приведёт к тому, что сначала появится надпись «Catch me if you can», а за ней «I will run no matter what!».

В реальном мире нам не нужно, чтобы throw запускал обработчик then. Решить это можно, скажем, возвращая Promise.reject() из функции:

async function getData() {
  try {
    if (true) {
      return Promise.reject("Catch me if you can");
    }
  } catch (err) {
    console.log(err.message);
  }
}
Now the error will be handled as expected:
getData()
  .then(() => console.log("I will NOT run no matter what!"))
  .catch(() => console.log("Catching err"));
"Catching err" // output

Помимо этого async/await выглядит лучшим способом структурирования асинхронного кода в JavaScript. Мы лучше управляем обработкой ошибок и код выглядит чище.

В любом случае, я не рекомендую рефакторить весь ваш JS-код под async/await. Обсудите это с командой. Но если вы работаете самостоятельно, то выбор между чистыми промисами и async/await — лишь дело вкуса.

11. JavaScript-движки: как они работают? Итоги

JavaScript — это скриптовый язык для веба, он сначала компилируется, а затем интерпретируется движком. Самые популярные JS-движки: V8, применяется в Google Chrome и Node.js; SpiderMonkey, разработан для Firefox; JavaScriptCore, используется в Safari.

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

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

Для упрощения работы асинхронного кода в ECMAScript 2015 были внедрены промисы. Промис — это асинхронный объект, используемый для представления успешности или неуспешности любой асинхронной операции. Но улучшения на этом не прекратились. В 2017-м появились async/await: стилистическое улучшение для промисов, позволяющее писать асинхронный код, как если бы он был синхронным.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Source: habr1

Метки:





Запись опубликована: 22.05.2019

JetBrains Open Day в Петербурге

Приглашаем всех на День открытых дверей в Петербурге!



Устроим большую тусовку в нашем новом офисе. Будут тематические зоны по технологиям, доклады, сессии ответов на вопросы, еда, музыка и даже открытый stand-up.

Мы хотели бы видеть у нас в гостях не только разработчиков, но и всех, кто связан с миром программирования: дизайнеров, девопсов, тестировщиков. Хотим, чтобы вы познакомились с нами, мы с вами, вы друг с другом, и все узнали что-то новое.

Докладов будет много, они будут происходить параллельно, так что каждый сможет составить себе расписание по вкусу. Вот некоторые из них:
Устранение барьеров на пути к продуктивности (на английском) — Хади Харири, руководитель Developer Advocacy.
IDE wars: увлекательные взаимодействия между нами, конкурентами, партнерами, платформами и сообществом — Кирилл Скрыган, тимлид команды Rider.

Клавиатуры и дизайн — Никита Прокопов, разработчик нового продукта.
Тестирование перформанса в Rider — Андрей Акиньшин, разработчик Rider.
Принципы дизайнеров интерфейсов в команде IntelliJ UX — Ольга Бердникова, дизайнер интерфейсов в платформе IntelliJ.

Rider C++ или Unreal Engine IDE, которую мы заслужили — Александр Пирогов, разработчик Rider С++.

Ближе к делу выкатим полный список.

У вас будет возможность познакомиться с ведущими разработчиками и СЕО JetBrains, послушать про жизнь и работу в компании, задать свои вопросы, поделиться идеями и, может быть, изменить свои карьерные планы.

Записывайте время и место:
Пятница, 28 июня 2019 года, с 14:00 до 23:00.
Петербургский офис JetBrains: Приморский проспект, 70.

Пожалуйста, зарегистрируйтесь. Нам нужно понимать, сколько готовить еды и места 🙂 Участие бесплатно. Ждём вас!

Команда JetBrains

Source: habr1

Метки:





Запись опубликована: 22.05.2019

[Перевод] Системы внутри картриджей: как инженеры расширяли возможности игровых консолей

image

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

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

Aladdin Deck Enhancer

В начале 1990-х в офисе компании Codemasters, должно быть, находилась впечатляющая лаборатория исследований и разработок. Изначально компания была создана с целью разработки игр для домашних компьютеров Commodore, но спустя несколько лет открыла отдел по созданию нелицензионных игр и аппаратных дополнений к Nintendo Entertainment System (NES). Наверно, самым знаменитым продуктом компании, по крайней мере, в среде консольных игроков, было чит-устройство Game Genie 1, но оно оказалось не единственным примером нелицензионного оборудования для NES.
Одним из самых потрясающих свойств видеоигровых картриджей является их превосходная универсальность. В процессе развития технологий картриджи становились всё мощнее и дополняли возможности самих консолей. Оборудование NES было разработано ещё в середине 1983 года, и к началу 90-х ему уже требовалась вся возможная помощь. Позже в картриджах NES появились всевозможные компоненты, в том числе ОЗУ, контроллеры для переключения банков памяти и специальные аппаратные мэпперы2.

Внутри некоторых картриджей для разных игр было одинаковое оборудование, единственное отличие заключалось в игровых данных, хранящихся на чипах ROM. Сооснователь компании Codemasters Ричард Дарлинг заметил это и к нему пришла блестящая идея создать дополнение для NES, позволяющее сделать игры проще и дешевле. Результатом реализации этой идеи стал Aladdin Deck Enhancer, выпущенный в ноябре 1992 года. Enhancer был картриджем NES, в котором содержалось всё, кроме игровых данных. Он имел разъём для подключения так называемых «компактных картриджей» (Compact Cartridge). Эти компактные картриджи должны были стать гораздо меньше обычных картриджей NES и состоять исключительно из игровых данных, записанных в чипах ROM.

Dizzy the Adventurer на Compact Cartridge.

Codemasters надеялась, что Enhancer сможет стать их собственной жизнеспособной целевой платформой для издателей, заинтересованных в обходе официальной программы лицензирования Nintendo и выпуске своих игр на компактных картриджах. Благодаря меньшим размерам и тому, что большая часть электрических схем, необходимых для обычных картриджей, уже была встроена непосредственно в Enhancer, компактные картриджи можно было продавать гораздо дешевле, чем обычные лицензированные игры NES. Учитывая то, что он был выпущен уже после появления на рынке преемника NES (SuperNES), очевидно, что целевой аудиторией Enhancer были ограниченные в средствах покупатели, которых больше интересовали не новые технологии, а просто игры.

К сожалению для Codemasters, идея так и не обрела успеха. Несмотря на обещания выпуска к концу 1993 года двадцати четырёх игр, было выпущено всего семь игр, и множество нераспроданных устройств на десятилетия осталось пылиться на складах. Невероятно, но совершенно новый, нераспечатанный Enhancer можно найти в Интернете даже спустя двадцать пять лет после выпуска3.

Super Game Boy

Super Game Boy был дополнением к Super Nintendo Entertainment System (Super NES), предоставлявшим изумительную возможность играть в игры для Game Boy. Размером не больше обычного картриджа для Super NES, Super Game Boy имел на верхней кромке разъём, в который можно было вставлять стандартные игры Game Boy. Вставив картридж Game Boy и присоединив Super Game Boy к консоли, игроки впервые получали шанс сыграть в любимые игры на телевизоре. Учитывая то, что Game Boy был печально известен своим зернистым, размытым и тусклым экраном, это казалось потрясающим усовершенствованием.

Современные игроки привыкли к совместимости старых игр с новыми системами, но для 90-х эта идея была довольно свежей4. Консоли 90-х не были достаточно мощными для программной эмуляции других систем, поэтому картридж Super Game Boy на самом деле сдержал всё внутреннее оборудование обычного Game Boy. Несмотря на то, что он по большей мере был просто консолью Game Boy в картридже, в рукаве у Super Game Boy было ещё несколько трюков.

Donkey Kong на Super Game Boy со специальной рамкой.

В игры Super NES пользователи должны были играть на больших телевизионных экранах, а не на дисплеях карманных устройств, поэтому их разрешение было выше, чему у игр Game Boy. Это означало, что при отображении игры Game Boy на экране телевизора её изображение должно было вставляться в рамку. Игроки могли выбрать одну из множества встроенных декоративных рамок или создавать собственные с помощью виртуальных инструментов рисования. Кроме того, игроки могли включать в игре цвета, заменяя монохромные оттенки цветов Game Boy на самостоятельно выбираемую палитру. Некоторые поздние игры для Game Boy даже выпускались с собственными встроенными рамками, и могли использовать аппаратные возможности Super Game Boy по отображению большего количества цветов игры5. К сожалению для геймеров такие усовершенствования не прижились, но те, которые всё-таки были выпущены, выглядят потрясающе6.

Super Game Boy был выпущен в 1994 году, через пять лет после начала цикла жизни Game Boy и спустя три года после выпуска Super NES. Во многих отношениях выпуск Super Game Boy был очень умным ходом. Во-первых, он значительно расширил библиотеку доступных для Super NES игр, мгновенно представив доступ к трём сотням дополнительных игр7. Во-вторых, он снова привлёк внимание к терявшему популярность Game Boy, который на тот момент начинал казаться устаревшим. Nintendo не стала долго ждать, чтобы дать Game Boy второе дыхание: первая игра про покемонов Pocket Monsters была выпущена в Японии в 1996, породив целый феномен поп-культуры, и к концу 1990-х Nintendo выпустила три дополнительных варианта консоли Game Boy8. В конечном итоге линейка Game Boy стала одной из лучших по продажам видеоигровых систем за всю историю9.
Super Game Boy 2 был выпущен в Японии в 1998 году. Он обновил свой стиль и соответствовал моде на «прозрачный пластик» конца 90-х, а также внёс множество усовершенствований. Первая версия Super Game Boy не совсем точно соответствовала скорости ЦП оригинальной портативной консоли, поэтому игры работали на 2,4% быстрее. В Super Game Boy 2 эта ошибка была исправлена; кроме того, в него добавили разъём для линк-кабеля Game Boy, позволявший при наличии второй консоли играть вдвоём10.

В целом Super Game Boy наверно оказался самой успешной «системой в картридже». Nintendo воспользовалась той же самой идеей в 2003 году для выпуска Game Boy Player, позволявшего играть на GameCube в игры для Game Boy Advance, Game Boy Color и первого Game Boy.

Sega 32X

Из трёх рассматриваемых нами систем Sega 32X меньше всего похожа на традиционный игровой картридж. Вставляемая в разъём картриджа Sega Genesis, 32X добавляла этой постепенно устаревающей системе значительные графические мощности. Нетрудно представить, что дизайнеры Sega пытались вызвать в покупателях ассоциации с турбонагнетателя мощного масл-кара, особенно учитывая основную целевую аудиторию системы — мальчиков подросткового возраста11.

В отличие от первых двух систем, 32X не была простым устройством вида «включи и играй». Перед подключением 32X игрокам нужно было установить внутрь Genesis металлические скобы. Затем нужно было подключить 32X к телевизору (не к Genesis), а затем подключить 32X к Genesis вторым видеокабелем. Наконец, у 32X был собственный источник питания с большим трансформатором, торчавшим из розетки. После подключения всей системы задняя часть конструкции из Genesis/32X представляла собой хаос из проводов12.

Выпущенная в 1994 году 32X появилась спустя пять с лишним лет после выпуска Genesis, которая в то время испытывала всё нарастающее давление со стороны и Nintendo, и других конкурентов с мощными консолями следующего поколения. Хотя собственная консоль Sega следующего поколения уже почти была готова к выпуску, она продавалась бы по цене следующего поколения, в более чем четыре раза превышая текущую стоимость Sega Genesis13.

Sega надеялась, что существует огромный потенциальный рынок потребителей, желающих выложить относительно небольшую сумму, чтобы стать чуть ближе к играм следующего поколения14, и казалось, что эта стратегия может сработать. Однако на следующий день после выпуска Sega 32X в Северной Америке в Японии вышла настоящая консоль следующего поколения под названием Saturn. Попытка привлечь разработчиков к одновременной поддержке двух новых систем была сложной задачей, и покупательский спрос на «полушаг» к следующему поколению консолей с каждым уходящим падал, ведь настоящее следующее поколение было уже совсем близко. Это давило на компанию Sega и заставило выпустить Saturn в Северной Америке раньше задуманного, всего спустя шесть месяцев после 32X. Это было катастрофой.

Тем не менее, нам несложно представить мир, в котором 32X оказалась бы успешной. Если бы 32X выпустили чуть раньше, или если бы Saturn выпустили чуть позже, то у 32X, возможно появилось бы время, чтобы достичь популярности15. Даже с учётом цены Genesis, 32X всё равно стоила в два раза дешевле Saturn, и могла многое предложить игрокам с точки зрения аппаратных улучшений. К сожалению, вместо того, чтобы стать фундаментом грядущего успеха Sega, 32X стала предзнаменованием постепенного ухода Sega с рынка разработки консолей.

Дополнение о дополнениях

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

Аппаратные дополнения встречаются и в современной истории видеоигр — вспомните например Sony, но сложности их использования не изменились. Дополнения — это всегда рулетка: их общий возможный рынок ограничен существующей базой пользователей основной системы, а к моменту выпуска дополнения на рынок игроки часто уже ждут следующее поколение технологий. Большинству дополнений не удаётся оказаться серьёзного влияния. Они они могут «выстрелить» при условии наличия больших преимуществ для игроков — успех Super Game Boy стал тому доказательством.

Примечания

  1. Более подробную информацию о Game Genie см. в предыдущей статье.
  2. Чипы мэпперов давали картриджам дополнительные преимущества, например, больший контроль над генерируемыми видеосигналами, улучшенный вывод звука или улучшенное управление памятью. Управление памятью было важным аспектом: ранние процессоры могли напрямую адресовать достаточно малый объём памяти, и чипы переключения банков памяти немного упрощали работу. Переключение банков — это технология, разбивающая большие области памяти на более удобные «банки», достаточно малые для обработки процессором. С помощью небольших хитростей программное обеспечение может при необходимости переключаться между банками и получать доступ к большему объёму памяти.
  3. На момент написания статьи (апрель 2019 года), совершенно новый Aladdin Deck Enhancer (нераспакованный!) продаётся на eBay менее чем за 100 долларов.
  4. Владельцы Sega Genesis могут играть в игры Master System с помощью устройства под названием Power Base Converter, а владельцы Atari 5200 могут играть в игры 2600 с помощью устройства CX-55.
  5. Кристин Лав опубликовала отличное исследование функционала Super Game Boy.
  6. Space Invaders для Game Boy имела совершенно отдельный вариант для Super NES, запускавшийся, когда картридж подключали с помощью Super Game Boy.
  7. Это число взято на основании Полного списка библиотеки игр Game Boy, и в нём учитываются все игры, выпущенные до 1994 года.
  8. Выпущенный в 1996 году Game Boy Pocket был более тонкой версией оригинальной консоли. За ним в начале 1998 года последовал Game Boy Light (только для японского рынка), в котором была добавлена подсветка экрана. Всего шесть месяцев спустя на сцену вышел Game Boy Color со значительно усовершенствованным «железом» и цветным ЖК-дисплеем (но, увы, без подсветки).
  9. По данным Википедии, соединённая линейка Game Boy/Game Boy Color стала второй по продажам портативной консолью за всю историю и третьей среди всех консолей.
  10. Линк-кабель Super Game Boy 2 работал и со стандартным Game Boy, и с другим Super Game Boy 2, если у игрока каким-то образом оказывались под рукой второй телевизор и Super NES.
  11. Согласно интервью с генеральным директором Sega of America Томом Калинске в Electronic Gaming Monthly (выпуск 65 за декабрь 1994 года, стр. 191), Sega of America активно участвовала в разработке оборудования 32X (а такое бывало редко), поэтому мы можем предположить, что в процессе работы учитывалась американская целевая аудитория/
  12. Подробный процесс сборки см в статье GameTrog How to hook up your SEGA Genesis 1 with the SEGA CD 1 and the SEGA 32X. (Заметьте, что здесь используется и дополнение Sega CD, что повышает общий хаос соединений.)
  13. Согласно рекламному объявлению в Sega Visions за август/сентябрь 1992 года, консоль Genesis продавалась за 99,99 доллара. В момент выпуска Saturn в 1995 году эта консоль продавалась за 399 долларов.
  14. «Я думаю, что многим людям нравятся игры Saturn, но они не могут себе их позволить. 32X может максимально приблизить их возможностям этой консоли». — цитата генерального директора Sega of America Тома Калинске из Electronic Gaming Monthly (выпуск 65 за декабрь 1994 года, стр. 191).
  15. Можно возразить, что у Sega CD были годы для того, чтобы получить популярность, и тем не менее она так и не «взлетела». Но всё-таки она стоила в два с лишним раза дороже 32X, поэтому нам сложно судить.

Дополнительное чтение

Платежная система

Поделиться публикацией

Source: habr1

Метки:





Запись опубликована: 22.05.2019

Статическое тестирование или спасти рядового Райана

Релиз часто подкрадывается незаметно. И любая ошибка, внезапно обнаруженная перед ним, грозит нам сдвигом сроков, хотфиксами, работой до утра и потраченными нервами. Когда подобный аврал стал происходить систематически, мы поняли, что так больше жить нельзя. Было решено разработать систему всесторонней валидации, чтобы спасти рядового Райана разработчика Артёма, который перед релизом уходил домой в 9 вечера, или в 10, или в 11… ну вы поняли. Идея была в том, чтобы разработчик узнавал об ошибке, пока изменения еще не попали в репозиторий, а он сам не потерял контекста задачи.


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

С чего всё начиналось

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

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

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

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

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

Наши усилия можно разделить на три направления:

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

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

Много требований — одна система

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

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

  • тип сборки: для релизных и отладочных ресурсов проверки будут отличаться по строгости, полноте охвата, а так же настройками идентификаторов и проверкой доступного функционала;
  • платформа: то, что валидно для андроида, может быть некорректно для iOS, ресурсы также собираются по-разному и не все ресурсы в android-версии будут в iOS и наоборот;
  • место запуска: где именно запускаем — на сборочном агенте, где нужны все доступные тесты или же на компьютере у пользователя, где список запускаемого необходимо минимизировать.

Система статических тестов

Ядро системы и основной набор статических тестов реализованы на python. Основу составляет всего лишь несколько сущностей:

Контекст тестирования — обширное понятие. В нём хранятся как параметры сборки и запуска, о которых мы говорили выше, так и мета-информация, которую заполняют и используют тесты.

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

К примеру, есть описание рыбки, в котором указана 3D-модель и текстура.Если файл с описанием изменился, значит проверяется, что указанная в нём модель и текстура существуют. В других случаях необходимости запуска проверки рыбки нет.

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

Следующая проблема — зависимость одних тестов от других: нельзя выполнить проверку рыбки перед нахождением всех текстур и моделей. Поэтому мы разделили всё выполнение на две стадии:

  • подготовка контекста
  • выполнение проверок

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

class VerificationContext(object):
   def __init__(self, app_path, build_type, platform, changed_files=None):
       self.__app_path = app_path
       self.__build_type = build_type
       self.__platform = platform
       # Заполняются запускающимися тестами
       self.__modified_resources = set()
       self.__expected_resources = set()
       # Если запуск происходит из прекомитного хука, тогда в этом списке будут изменённые файлы
       self.__changed_files = changed_files
       # Мета-данные о ресурсах, которые нашли тесты
       self.__resources = {}
def expect_resources(self, resources):
   self.__expected_resources.update(resources)
def is_resource_expected(self, resource):
   return resource in self.__expected_resources
def register_resource(self, resource_type, resource_id, resource_data=None):
   self.__resources.setdefault(resource_type, {})[resource_id] = resource_data
def get_resource(self, resource_type, resource_id):
   if resource_type not in self.__resources or resource_id not in self.__resources[resource_type]:
       return None, None
   return resource_id, self.__resources[resource_type][resource_id]

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

class TestCase(object):
   def __init__(self, name, context, build_types=None, platforms=None, predicate=None,
                expected_resources=None, modified_resources=None):
       self.__name = name
       self.__context = context
       self.__build_types = build_types
       self.__platforms = platforms
       self.__predicate = predicate
       self.__expected_resources = expected_resources
       self.__modified_resources = modified_resources
       # Подходит ли тип сборки и платформы для запуска теста
       # Изменились ли ресурсы, за которые отвечает предикат
       self.__need_run = self.__check_run()
       self.__need_resource_run = False
   @property
   def context(self):
       return self.__context
   def fail(self, message):
       print('Fail: {}'.format(message))
   def __check_run(self):
       build_success = self.__build_types is None or self.__context.build_type in self.__build_types
       platform_success = self.__platforms is None or self.__context.platform in self.__platforms
       hook_success = build_success
       if build_success and self.__context.is_build('hook') and self.__predicate:
           hook_success = any(self.__predicate(changed_file) for changed_file in self.__context.changed_files)
       return build_success and platform_success and hook_success
   def __set_context_resources(self):
       if not self.__need_run:
           return
       if self.__modified_resources:
           self.__context.modify_resources(self.__modified_resources)
       if self.__expected_resources:
           self.__context.expect_resources(self.__expected_resources)
    def init(self):
       """
       Запускается после того, как создались все тесты и в контекст записана информация
       об изменённых ресурсах и тех ресурах, которые нужны другим тестам
       """
       self.__need_resource_run = self.__modified_resources and any(self.__context.is_resource_expected(resource) for resource in self.__modified_resources)
   def _prepare_impl(self):
       pass
   def prepare(self):
       if not self.__need_run and not self.__need_resource_run:
           return
       self._prepare_impl()
   def _run_impl(self):
       pass
   def run(self):
       if self.__need_run:
           self._run_impl()

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

class VerifyTexture(TestCase):
   def __init__(self, context):
       super(VerifyTexture, self).__init__('VerifyTexture', context,
                                           build_types=['production', 'hook'],
                                           platforms=['windows', 'ios'],
                                           expected_resources=None,
                                           modified_resources=['Texture'],
                                           predicate=lambda file_path: os.path.splitext(file_path)[1] == '.png')
   def _prepare_impl(self):
       texture_dir = os.path.join(self.context.app_path, 'resources', 'textures')
       for root, dirs, files in os.walk(texture_dir):
           for tex_file in files:
               self.context.register_resource('Texture', tex_file)
class VerifyModels(TestCase):
   def __init__(self, context):
       super(VerifyModels, self).__init__('VerifyModels', context,
                                          expected_resources=['Texture'],
                                          predicate=lambda file_path: os.path.splitext(file_path)[1] == '.obj')
   def _run_impl(self):
       models_descriptions = etree.parse(os.path.join(self.context.app_path, 'resources', 'models.xml'))
       for model_xml in models_descriptions.findall('.//Model'):
           texture_id = model_xml.get('texture')
           texture = self.context.get_resource('Texture', texture_id)
           if texture is None:
               self.fail('Texture for model {} was not found: {}'.format(model_xml.get('id'), texture_id))

Распространение на проекты

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

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

class Runner(object):
   def __init__(self, config_str, setup_function):
       self.__tests = []
       config_parser = RawConfigParser()
       config_parser.read_string(config_str)
       app_path = config_parser.get('main', 'app_path')
       build_type = config_parser.get('main', 'build_type')
       platform = config_parser.get('main', 'platform')
       '''
       get_changed_files возвращает список изменённых файлов и зависит от используемой CVS
       '''
       changed_files = None if build_type != 'hook' else get_changed_files()
       self.__context = VerificationContext(app_path, build_type, platform, changed_files)
       setup_function(self)
   @property
   def context(self):
       return self.__context
   def add_test(self, test):
       self.__tests.append(test)
   def run(self):
       for test in self.__tests:
           test.init()
       for test in self.__tests:
           test.prepare()
       for test in self.__tests:
           test.run()

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

Пример файла конфигурации

[main]
app_path = {app_path}
build_type = production
platform = ios

Пример настроечной xml

<root>
	<VerifySourceCodepage allow_utf8="true" allow_utf8Bom="false" autofix_path="ci/autofix">
		<IgnoreFiles>*android/tmp/*</IgnoreFiles>
	</VerifySourceCodepage>
	<VerifyCodeStructures>
		<Checker name="NsStringConversion" />
		<Checker name="LogConstructions" />
	</VerifyCodeStructures>
</root>

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

def setup(runner):
   runner.add_test(VerifyTexture(runner.context))
   runner.add_test(VerifyModels(runner.context))
def run():
   raw_config = '''
   [main]
   app_path = {app_path}
   build_type = production
   platform = ios
   '''
   runner = Runner(raw_config, setup)
   runner.run()

Собранные грабли

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

  1. Python и все пакеты устанавливает сам пользователь. Но есть два «но»: не все пользователи — программисты и установка через pip install для дизайнеров, да и для программистов тоже, может стать проблемой.
  2. Есть скрипт, который устанавливает все необходимые пакеты. Это уже лучше, но если у пользователя установлен не тот питон, то могут возникать коллизии в работе.
  3. Доставка нужной версии интерпретатора и зависимостей из хранилища артефактов (Nexus) и выполнение тестов в виртуальном окружении.

Другая проблема — быстродействие. Чем больше тестов, тем суммарно дольше происходит проверка изменений на компьютере пользователя. Раз в несколько месяцев идёт профилирование и оптимизация узких мест. Так был доработан контекст, появился кеш для текстовых файлов, доработаны механизмы предикатов (определения, что этот файл интересен тесту).

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

Заключение

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

Полный пример кода, используемого в статье в качестве примеров, можно посмотреть в нашем репозитории.

Source: habr1

Метки:





Запись опубликована: 22.05.2019

[Перевод] Selenium WebDriver – Метрика тестов в реальном времени с использованием Grafana и InfluxDB

Всем привет! Уже на следующей неделе стартуют занятия в группе «Java QA Engineer». Этому и будет приурочена нынешняя публикация.

Обзор:

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

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

Цель:

Мы будет собирать все доступные тестовые метрики, такие как:

  • Статус теста-метода
  • Продолжительность теста-метода
  • Статус класса с тестовыми методами
  • Время выполнения тестов одного класса
  • Статус набора тестов
  • Продолжительность выполнения набора тестов

Некоторые из показателей мы можем получить в режиме реального времени, как показано ниже:

  • Количество методов тестирования, сгруппированных по статусу (например: Pass: 30, Failed: 2, Skipped: 2) в конкретный день.
  • Тенденция продолжительности выполнения набора тестов за неделю, месяц, год и т.д.

InfluxDB:

InfluxDB – это база данных временных рядов, которая используется для сбора всех метрик тестов. InfluxDB имеет REST API для записи данных и отправки запросов. Больше вы можете узнать . Ниже я использую команду docker для запуска экземпляра InfluxDB.

sudo docker run -p 8086:8086 -v $PWD:/var/lib/influxdb influxdb

Создание базы данных:

У нас уже поднята и запущена база данных InfluxDB. Давайте создадим отдельную схему базы данных для сбора результатов тестов Selenium. Ниже я запускаю команду в терминале, чтобы создать в базе данных схему под названием «selenium». (Проверьте URL адрес, замените localhost на hostname/ipaddress, если запускаете не на текущем компьютере).

curl -i -XPOST http://localhost:8086/query --data-urlencode "q=CREATE DATABASE selenium"

TestNG:

Давайте создадим простой тест testNG:

public class SampleTest {
    @Test(description = "login")
    public void login(){
    }
    @Test(description = "search for flights", dependsOnMethods = "login")
    public void search(){
    }
    @Test(description = "select flight", dependsOnMethods = "search")
    public void select(){
    }
    @Test(description = "book flight", dependsOnMethods = "select")
    public void book(){
    }
    @Test(description = "logout", dependsOnMethods = "book")
    public void logout(){
    }
}

Нашей целью является сбор результатов теста в InfluxDB во время выполнения. Итак, нам понадобится драйвер/библиотека в Java для InfluxDB.

Зависимости Maven:

Добавьте зависимости Maven, указанные далее:

<dependency>
            <groupId>org.influxdb</groupId>
            <artifactId>influxdb-java</artifactId>
            <version>2.12</version>
 </dependency>

Слушатели:

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

import org.influxdb.InfluxDB;
import org.influxdb.InfluxDBFactory;
import org.influxdb.dto.Point;
public class ResultSender {
    private static final InfluxDB INFLXUDB = InfluxDBFactory.connect("http://localhost:8086", "root", "root");
    private static final String DATABASE = "selenium";
    static{
        INFLXUDB.setDatabase(DATABASE);
    }
    public static void send(final Point point){
        INFLXUDB.write(point);
    }
}

Теперь создадим другой класс, который реализует интерфейс ITestListener.

import org.influxdb.dto.Point;
import org.testng.ITestContext;
import org.testng.ITestListener;
import org.testng.ITestResult;
import java.util.concurrent.TimeUnit;
public class ExecutionListener implements ITestListener {
    public void onTestStart(ITestResult iTestResult) {
    }
    public void onTestSuccess(ITestResult iTestResult) {
        this.sendTestMethodStatus(iTestResult, "PASS");
    }
    public void onTestFailure(ITestResult iTestResult) {
        this.sendTestMethodStatus(iTestResult, "FAIL");
    }
    public void onTestSkipped(ITestResult iTestResult) {
        this.sendTestMethodStatus(iTestResult, "SKIPPED");
    }
    public void onTestFailedButWithinSuccessPercentage(ITestResult iTestResult) {
    }
    public void onStart(ITestContext iTestContext) {
    }
    public void onFinish(ITestContext iTestContext) {
        this.sendTestClassStatus(iTestContext);
    }
    private void sendTestMethodStatus(ITestResult iTestResult, String status) {
        Point point = Point.measurement("testmethod")
                .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                .tag("testclass", iTestResult.getTestClass().getName())
                .tag("name", iTestResult.getName())
                .tag("description", iTestResult.getMethod().getDescription())
                .tag("result", status)
                .addField("duration", (iTestResult.getEndMillis() - iTestResult.getStartMillis()))
                .build();
        ResultSender.send(point);
    }
    private void sendTestClassStatus(ITestContext iTestContext) {
        Point point = Point.measurement("testclass")
                .time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
                .tag("name", iTestContext.getAllTestMethods()[0].getTestClass().getName())
                .addField("duration", (iTestContext.getEndDate().getTime() - iTestContext.getStartDate().getTime()))
                .build();
        ResultSender.send(point);
    }
}

Примечание: Используйте дополнительный тег, отвечающий вашим задачам в приведенном выше примере, чтобы классифицировать результаты. Например tag(“scenario”, “login flow”).

Слушатель (listener) из примера выше будет мониторить выполнение теста и как только определенный метод/класс теста будет выполнен, он отправит имя, продолжительность и кое-какие дополнительные детали. Моя цель здесь – просто подкинуть вам идею. Меняйте код в соответствии с вашими нуждами.

Теперь добавьте слушателя в XML-пакет или в базовый класс TestNG.

<suite name="My suite">
    <listeners>
        <listener class-name="com.tag.realtime.ExecutionListener" />
    </listeners>
    <test name="Test1">
        <classes>
            <class name="com.tag.realtime.SampleTest"/>
        </classes>
    </test>
    <test name="Test2">
        <classes>
            <class name="com.tag.realtime.Sample2Test"/>
        </classes>
    </test>
</suite>

Или же:

@Listeners(ExecutionListener.class)
public class SampleTest {
    @Test
    public void test(){
    }
}

Grafana:

Мы добились отправки результатов в InfluxDB. Но как запрашивать результаты и визуализировать полученные данные? Для этого мы будет использовать другой бесплатный инструмент под названием “Grafana”.
Grafana является отличным инструментом визуализации для данных временных рядов, она прекрасно взаимодействует с InfluxDB. Ниже приведены команды docker для создания экземпляра Grafana. [плагин piechart является необязательным в команде, его можно удалить, если в нем нет надобности]

docker run -d -p 3000:3000
  --name=grafana 
  -e "GF_INSTALL_PLUGINS=grafana-piechart-panel" 
  -v $PWD:/var/lib/grafana 
  grafana/grafana

Источник данных для Grafana:

Перейдите в Settings -> Data sources -> Add new data source, как показано на скриншоте. Нажмите на кнопку ‘Save & Test’, чтобы удостовериться, что Grafana может общаться с InfluxDB.

Примечание: Если вы используете Grafana с Docker и пытаетесь получить доступ как ‘Server default’, НЕ ИСПОЛЬЗУЙТЕ localhost в строке подключения к InfluxDB. Все потому, что здесь localhost – это контейнер Grafana, а не физическая машина. Таким образом контейнер Grafana не сможет найти InfluxDB.

Создание панели мониторинга:

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

Демо 2:

Подведем итог:

Надеюсь, получение результатов в реальном времени с помощью InfluxDB и Grafana оказалось вам интересно и полезно. Оно требует минимальных изменений в существующей структуре, ведь мы используем слушателей TestNG. Удалить слушателя из файла набора/базового класса достаточно, чтобы отключить эту функцию, если вам она не нужна. Такой подход поможет избежать некой фрустрации в команде, если ее члены только и занимаются тем, что мониторят результаты тестов через консольный ввод/вывод на удаленной машине. В этой статье лишь изложена основная идея. Вы можете улучшить этот подход, добавив больше информации, например, тестовую среду, добавить дополнительные фильтры для обновления данных в диаграммах для конкретной среды/тестов и т.д.

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

Похожие публикации

Source: habr1

Метки: