/*
    V4L2 API compliance input/output configuration ioctl tests.

    Copyright (C) 2011  Hans Verkuil <hverkuil@xs4all.nl>

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA  02110-1335  USA
 */

#include <map>
#include <set>

#include <sys/types.h>

#include "v4l2-compliance.h"

static int checkStd(struct node *node, bool has_std, v4l2_std_id mask, bool is_input)
{
	v4l2_std_id std_mask = 0;
	v4l2_std_id std = 0;
	struct v4l2_standard enumstd;
	unsigned i;
	int ret;

	ret = doioctl(node, VIDIOC_G_STD, &std);
	if (ret && has_std)
		return fail("STD cap set, but could not get standard\n");
	if (!ret && !has_std)
		return fail("STD cap not set, but could still get a standard\n");
	if (ret != ENOTTY && ret != ENODATA && !has_std)
		return fail("STD cap not set, but got wrong error code (%d)\n", ret);
	if (!ret && has_std) {
		if (std & ~mask)
			warn("current standard is invalid according to the standard mask\n");
		if (std == 0)
			return fail("Standard == 0?!\n");
		if (std & V4L2_STD_ATSC)
			return fail("Current standard contains ATSC standards. This is no longer supported\n");
	}
	ret = doioctl(node, VIDIOC_S_STD, &std);
	if (ret && has_std)
		return fail("STD cap set, but could not set standard\n");
	if (!ret && !has_std)
		return fail("STD cap not set, but could still set a standard\n");
	std = V4L2_STD_ATSC;
	ret = doioctl(node, VIDIOC_S_STD, &std);
	if (ret != ENODATA && ret != EINVAL && ret != ENOTTY)
		return fail("could set standard to ATSC, which is not supported anymore\n");
	for (i = 0; ; i++) {
		memset(&enumstd, 0xff, sizeof(enumstd));

		enumstd.index = i;
		ret = doioctl(node, VIDIOC_ENUMSTD, &enumstd);
		if (ret)
			break;
		std_mask |= enumstd.id;
		if (check_ustring(enumstd.name, sizeof(enumstd.name)))
			return fail("invalid standard name\n");
		if (check_0(enumstd.reserved, sizeof(enumstd.reserved)))
			return fail("reserved not zeroed\n");
		if (enumstd.framelines == 0)
			return fail("framelines not filled\n");
		if (enumstd.framelines != 625 && enumstd.framelines != 525)
			warn("strange framelines value %d for standard %08llx\n",
					enumstd.framelines, enumstd.id);
		if (enumstd.frameperiod.numerator == 0 || enumstd.frameperiod.denominator == 0)
			return fail("frameperiod is not filled\n");
		if (enumstd.index != i)
			return fail("index changed!\n");
		if (enumstd.id == 0)
			return fail("empty standard returned\n");
	}
	if (i == 0 && has_std)
		return fail("STD cap set, but no standards can be enumerated\n");
	if (i && !has_std)
		return fail("STD cap was not set, but standards can be enumerated\n");
	if (ret != ENOTTY && ret != ENODATA && !has_std)
		return fail("STD cap not set, but got wrong error code for enumeration (%d)\n", ret);
	if (std_mask & V4L2_STD_ATSC)
		return fail("STD mask contains ATSC standards. This is no longer supported\n");
	if (has_std && std_mask != mask)
		return fail("the union of ENUMSTD does not match the standard mask (%llx != %llx)\n",
				std_mask, mask);
	ret = doioctl(node, VIDIOC_QUERYSTD, &std);
	if (!ret && !has_std)
		return fail("STD cap was not set, but could still query standard\n");
	if (ret != ENOTTY && ret != ENODATA && !has_std)
		return fail("STD cap not set, but got wrong error code for query (%d)\n", ret);
	if (ret != ENOTTY && !is_input)
		return fail("this is an output, but could still query standard\n");
	if (!ret && is_input && (std & ~std_mask))
		return fail("QUERYSTD gives back an unsupported standard\n");
	return 0;
}

int testStd(struct node *node)
{
	int ret;
	unsigned i, o;
	bool has_std = false;

	for (i = 0; i < node->inputs; i++) {
		struct v4l2_input input;

		input.index = i;
		ret = doioctl(node, VIDIOC_ENUMINPUT, &input);
		if (ret)
			return fail("could not enumerate input %d?!\n", i);
		ret = doioctl(node, VIDIOC_S_INPUT, &input.index);
		if (ret)
			return fail("could not select input %d.\n", i);
		if (input.capabilities & V4L2_IN_CAP_STD)
			has_std = true;
		if (checkStd(node, input.capabilities & V4L2_IN_CAP_STD, input.std, true))
			return fail("STD failed for input %d.\n", i);
	}

	for (o = 0; o < node->outputs; o++) {
		struct v4l2_output output;

		output.index = o;
		ret = doioctl(node, VIDIOC_ENUMOUTPUT, &output);
		if (ret)
			return fail("could not enumerate output %d?!\n", o);
		ret = doioctl(node, VIDIOC_S_OUTPUT, &output.index);
		if (ret)
			return fail("could not select output %d.\n", o);
		if (output.capabilities & V4L2_OUT_CAP_STD)
			has_std = true;
		if (checkStd(node, output.capabilities & V4L2_OUT_CAP_STD, output.std, false))
			return fail("STD failed for output %d.\n", o);
	}
	return has_std ? 0 : ENOTTY;
}

static int checkTimings(struct node *node, bool has_timings, bool is_input)
{
	struct v4l2_enum_dv_timings enumtimings;
	struct v4l2_dv_timings timings;
	struct v4l2_format fmt = { 0 };
	bool is_mplane = node->g_caps() & (V4L2_CAP_VIDEO_CAPTURE_MPLANE |
				       V4L2_CAP_VIDEO_OUTPUT_MPLANE |
				       V4L2_CAP_VIDEO_M2M_MPLANE);
	unsigned type;
	unsigned i;
	int ret;

	memset(&timings, 0xff, sizeof(timings));
	if (node->can_capture)
		type = is_mplane ? V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE : V4L2_BUF_TYPE_VIDEO_CAPTURE;
	else
		type = is_mplane ? V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE : V4L2_BUF_TYPE_VIDEO_OUTPUT;
	ret = doioctl(node, VIDIOC_G_DV_TIMINGS, &timings);
	if (ret && has_timings)
		return fail("TIMINGS cap set, but could not get current timings\n");
	if (!ret && !has_timings)
		return fail("TIMINGS cap not set, but could still get timings\n");
	if (ret != ENOTTY && ret != ENODATA && !has_timings)
		return fail("TIMINGS cap not set, but got wrong error code (%d)\n", ret);

	if (!ret) {
		fail_on_test(timings.type != V4L2_DV_BT_656_1120);
		fail_on_test(check_0(timings.bt.reserved, sizeof(timings.bt.reserved)));
	}

	for (i = 0; ; i++) {
		memset(&enumtimings, 0xff, sizeof(enumtimings));

		enumtimings.index = i;
		enumtimings.pad = 0;
		ret = doioctl(node, VIDIOC_ENUM_DV_TIMINGS, &enumtimings);
		if (ret)
			break;
		if (check_0(enumtimings.reserved, sizeof(enumtimings.reserved)))
			return fail("reserved not zeroed\n");
		fail_on_test(check_0(enumtimings.timings.bt.reserved,
				     sizeof(enumtimings.timings.bt.reserved)));
		if (enumtimings.index != i)
			return fail("index changed!\n");
		memset(enumtimings.timings.bt.reserved, 0xff,
		       sizeof(enumtimings.timings.bt.reserved));
		fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &enumtimings.timings));
		fail_on_test(check_0(enumtimings.timings.bt.reserved,
				     sizeof(enumtimings.timings.bt.reserved)));

		struct v4l2_dv_timings g_timings;
		fail_on_test(doioctl(node, VIDIOC_G_DV_TIMINGS, &g_timings));
		fail_on_test(g_timings.bt.width != enumtimings.timings.bt.width);
		fail_on_test(g_timings.bt.height != enumtimings.timings.bt.height);

		if (node->is_vbi || node->is_meta)
			continue;
		fmt.type = type;
		fail_on_test(doioctl(node, VIDIOC_G_FMT, &fmt));

		unsigned field = is_mplane ? fmt.fmt.pix_mp.field : fmt.fmt.pix.field;

		if (enumtimings.timings.bt.interlaced)
			fail_on_test(field == V4L2_FIELD_NONE);
		else
			fail_on_test(field != V4L2_FIELD_NONE);

		unsigned factor = V4L2_FIELD_HAS_T_OR_B(field) ? 2 : 1;

		// Test if the new format after setting new timings is either too small
		// or more than 1.5 times the width/height. This allows for some
		// adjustments to be made to the format, but still detect if the
		// format isn't updated by the driver.
		if (is_mplane) {
			fail_on_test(fmt.fmt.pix_mp.width < enumtimings.timings.bt.width);
			fail_on_test(fmt.fmt.pix_mp.width >= enumtimings.timings.bt.width * 1.5);
			fail_on_test(fmt.fmt.pix_mp.height * factor < enumtimings.timings.bt.height);
			fail_on_test(fmt.fmt.pix_mp.height * factor >= enumtimings.timings.bt.height * 1.5);
		} else {
			fail_on_test(fmt.fmt.pix.width < enumtimings.timings.bt.width);
			fail_on_test(fmt.fmt.pix.width >= enumtimings.timings.bt.width * 1.5);
			fail_on_test(fmt.fmt.pix.height * factor < enumtimings.timings.bt.height);
			fail_on_test(fmt.fmt.pix.height * factor >= enumtimings.timings.bt.height * 1.5);
		}
	}
	if (i == 0 && has_timings)
		return fail("TIMINGS cap set, but no timings can be enumerated\n");
	if (i && !has_timings)
		return fail("TIMINGS cap was not set, but timings can be enumerated\n");
	if (ret != ENOTTY && ret != ENODATA && !has_timings)
		return fail("TIMINGS cap not set, but got wrong error code for enumeration (%d)\n", ret);
	if (has_timings)
		fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &timings));
	ret = doioctl(node, VIDIOC_QUERY_DV_TIMINGS, &timings);
	if (!ret)
		fail_on_test(check_0(timings.bt.reserved, sizeof(timings.bt.reserved)));
	if (!ret && !has_timings)
		return fail("TIMINGS cap was not set, but could still query timings\n");
	if (ret != ENOTTY && ret != ENODATA && !has_timings)
		return fail("TIMINGS cap not set, but got wrong error code for query (%d)\n", ret);
	if (ret != ENOTTY && !is_input)
		return fail("this is an output, but could still query timings\n");
	return 0;
}

static int checkSubDevEnumTimings(struct node *node, __u32 pad)
{
	struct v4l2_enum_dv_timings enumtimings;
	struct v4l2_dv_timings timings;
	bool found_fmt_mismatch = false;
	bool has_timings;
	int ret;

	memset(&timings, 0xff, sizeof(timings));
	ret = doioctl(node, VIDIOC_G_DV_TIMINGS, &timings);
	has_timings = ret != ENOTTY;
	fail_on_test(has_timings && ret);
	if (has_timings) {
		fail_on_test(check_0(timings.bt.reserved, sizeof(timings.bt.reserved)));
		memset(timings.bt.reserved, 0xff, sizeof(timings.bt.reserved));
		fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &timings));
		fail_on_test(check_0(timings.bt.reserved, sizeof(timings.bt.reserved)));
	} else {
		fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &timings) != ENOTTY);
		fail_on_test(doioctl(node, VIDIOC_QUERY_DV_TIMINGS, &timings) != ENOTTY);
	}

	for (unsigned i = 0; ; i++) {
		memset(&enumtimings, 0xff, sizeof(enumtimings));

		enumtimings.index = i;
		enumtimings.pad = pad;
		ret = doioctl(node, VIDIOC_ENUM_DV_TIMINGS, &enumtimings);
		fail_on_test(ret != ENOTTY && !has_timings);
		fail_on_test(ret == ENOTTY && has_timings);
		if (ret == ENOTTY)
			return ret;
		fail_on_test(ret && ret != EINVAL);
		fail_on_test(ret && !i);
		if (ret)
			break;
		fail_on_test(check_0(enumtimings.reserved, sizeof(enumtimings.reserved)));
		fail_on_test(check_0(enumtimings.timings.bt.reserved,
				     sizeof(enumtimings.timings.bt.reserved)));
		fail_on_test(enumtimings.index != i);
		fail_on_test(enumtimings.pad != pad);
		memset(enumtimings.timings.bt.reserved, 0xff,
		       sizeof(enumtimings.timings.bt.reserved));
		fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &enumtimings.timings));
		fail_on_test(check_0(enumtimings.timings.bt.reserved,
				     sizeof(enumtimings.timings.bt.reserved)));

		struct v4l2_dv_timings g_timings;

		fail_on_test(doioctl(node, VIDIOC_G_DV_TIMINGS, &g_timings));
		fail_on_test(g_timings.bt.width != enumtimings.timings.bt.width);
		fail_on_test(g_timings.bt.height != enumtimings.timings.bt.height);

		if (found_fmt_mismatch)
			continue;

		struct v4l2_subdev_format fmt;

		memset(&fmt, 0, sizeof(fmt));
		fmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;

		// Test if the new format after setting new timings is either too small
		// or more than 1.5 times the width/height. This allows for some
		// adjustments to be made to the format, but still detect if the
		// format isn't updated by the driver.
		if (!doioctl(node, VIDIOC_SUBDEV_G_FMT, &fmt)) {
			if (V4L2_FIELD_HAS_T_OR_B(fmt.format.field))
				fmt.format.height *= 2;

			if (fmt.format.width < enumtimings.timings.bt.width) {
				warn("Active width for pad 0 is %u, which is less than the timings width of %u\n",
				     fmt.format.width, enumtimings.timings.bt.width);
				found_fmt_mismatch = true;
			}
			if (fmt.format.width >= enumtimings.timings.bt.width * 1.5) {
				warn("Active width for pad 0 is %u, which is more than 1.5 * the timings width of %u\n",
				     fmt.format.width, enumtimings.timings.bt.width);
				found_fmt_mismatch = true;
			}
			if (fmt.format.height < enumtimings.timings.bt.height) {
				warn("Active height for pad 0 is %u, which is less than the timings height of %u\n",
				     fmt.format.height, enumtimings.timings.bt.height);
				found_fmt_mismatch = true;
			}
			if (fmt.format.height >= enumtimings.timings.bt.height * 1.5) {
				warn("Active height for pad 0 is %u, which is more than 1.5 * the timings height of %u\n",
				     fmt.format.height, enumtimings.timings.bt.height);
				found_fmt_mismatch = true;
			}
		}
	}
	enumtimings.pad = node->entity.pads;
	enumtimings.index = 0;
	fail_on_test(doioctl(node, VIDIOC_ENUM_DV_TIMINGS, &enumtimings) != EINVAL);

	fail_on_test(doioctl(node, VIDIOC_S_DV_TIMINGS, &timings));
	memset(&timings, 0xff, sizeof(timings));
	ret = doioctl(node, VIDIOC_QUERY_DV_TIMINGS, &timings);
	fail_on_test(ret == ENOTTY);
	fail_on_test(ret == ENODATA);
	if (!ret || ret == ERANGE)
		fail_on_test(check_0(timings.bt.reserved, sizeof(timings.bt.reserved)));
	return 0;
}

int testTimings(struct node *node)
{
	int ret;
	unsigned i, o;
	bool has_timings = false;

	if (node->is_subdev()) {
		for (unsigned pad = 0; pad < node->entity.pads; pad++) {
			ret = checkSubDevEnumTimings(node, pad);
			if (!ret)
				has_timings = true;
			fail_on_test(ret && ret != ENOTTY);
		}
		return has_timings ? 0 : ENOTTY;
	}

	for (i = 0; i < node->inputs; i++) {
		struct v4l2_input input;

		input.index = i;
		ret = doioctl(node, VIDIOC_ENUMINPUT, &input);
		if (ret)
			return fail("could not enumerate input %d?!\n", i);
		ret = doioctl(node, VIDIOC_S_INPUT, &input.index);
		if (ret)
			return fail("could not select input %d.\n", i);
		if (input.capabilities & V4L2_IN_CAP_DV_TIMINGS)
			has_timings = true;
		if (checkTimings(node, input.capabilities & V4L2_IN_CAP_DV_TIMINGS, true))
			return fail("Timings check failed for input %d.\n", i);
	}

	for (o = 0; o < node->outputs; o++) {
		struct v4l2_output output;

		output.index = o;
		ret = doioctl(node, VIDIOC_ENUMOUTPUT, &output);
		if (ret)
			return fail("could not enumerate output %d?!\n", o);
		ret = doioctl(node, VIDIOC_S_OUTPUT, &output.index);
		if (ret)
			return fail("could not select output %d.\n", o);
		if (output.capabilities & V4L2_OUT_CAP_DV_TIMINGS)
			has_timings = true;
		if (checkTimings(node, output.capabilities & V4L2_OUT_CAP_DV_TIMINGS, false))
			return fail("Timings check failed for output %d.\n", o);
	}
	return has_timings ? 0 : ENOTTY;
}

static int checkTimingsCap(struct node *node, unsigned pad, bool has_timings)
{
	struct v4l2_dv_timings_cap timingscap;
	int ret;

	memset(&timingscap, 0xff, sizeof(timingscap));
	timingscap.pad = pad;
	ret = doioctl(node, VIDIOC_DV_TIMINGS_CAP, &timingscap);
	if (node->is_subdev() && ret == ENOTTY)
		return ENOTTY;
	if (ret && has_timings)
		return fail("TIMINGS cap set, but could not get timings caps\n");
	if (!ret && !has_timings)
		return fail("TIMINGS cap not set, but could still get timings caps\n");
	if (ret && !has_timings)
		return 0;
	if (check_0(timingscap.reserved, sizeof(timingscap.reserved)))
		return fail("reserved not zeroed\n");
	fail_on_test(timingscap.pad != pad);
	fail_on_test(timingscap.type != V4L2_DV_BT_656_1120);
	if (check_0(timingscap.bt.reserved, sizeof(timingscap.bt.reserved)))
		return fail("reserved not zeroed\n");
	fail_on_test(timingscap.bt.min_width > timingscap.bt.max_width);
	fail_on_test(timingscap.bt.min_height > timingscap.bt.max_height);
	fail_on_test(timingscap.bt.min_pixelclock > timingscap.bt.max_pixelclock);

	if (!node->is_subdev())
		return 0;

	memset(&timingscap, 0, sizeof(timingscap));
	timingscap.pad = node->is_subdev() ? node->entity.pads : 1;
	fail_on_test(doioctl(node, VIDIOC_DV_TIMINGS_CAP, &timingscap) != EINVAL);
	return 0;
}

int testTimingsCap(struct node *node)
{
	int ret;
	unsigned i, o;
	bool has_timings = false;

	if (node->is_subdev()) {
		for (unsigned pad = 0; pad < node->entity.pads; pad++) {
			bool is_input = node->pads[pad].flags & MEDIA_PAD_FL_SINK;

			ret = checkTimingsCap(node, pad, true);
			if (ret && ret != ENOTTY)
				return fail("EDID check failed for %s pad %u.\n",
					    is_input ? "sink" : "source", pad);
			if (!ret)
				has_timings = true;
		}
		return has_timings ? 0 : ENOTTY;
	}

	for (i = 0; i < node->inputs; i++) {
		struct v4l2_input input;

		input.index = i;
		ret = doioctl(node, VIDIOC_ENUMINPUT, &input);
		if (ret)
			return fail("could not enumerate input %d?!\n", i);
		ret = doioctl(node, VIDIOC_S_INPUT, &input.index);
		if (ret)
			return fail("could not select input %d.\n", i);
		if (input.capabilities & V4L2_IN_CAP_DV_TIMINGS)
			has_timings = true;
		if (checkTimingsCap(node, 0, input.capabilities & V4L2_IN_CAP_DV_TIMINGS))
			return fail("Timings cap failed for input %d.\n", i);
	}

	for (o = 0; o < node->outputs; o++) {
		struct v4l2_output output;

		output.index = o;
		ret = doioctl(node, VIDIOC_ENUMOUTPUT, &output);
		if (ret)
			return fail("could not enumerate output %d?!\n", o);
		ret = doioctl(node, VIDIOC_S_OUTPUT, &output.index);
		if (ret)
			return fail("could not select output %d.\n", o);
		if (output.capabilities & V4L2_OUT_CAP_DV_TIMINGS)
			has_timings = true;
		if (checkTimingsCap(node, 0, output.capabilities & V4L2_OUT_CAP_DV_TIMINGS))
			return fail("Timings cap check failed for output %d.\n", o);
	}
	return has_timings ? 0 : ENOTTY;
}

static int checkEdid(struct node *node, unsigned pad, bool is_input)
{
	struct v4l2_edid edid;
	__u8 data[256 * 128] = { 0 };
	unsigned blocks;
	bool has_edid;
	int ret;

	memset(edid.reserved, 0xff, sizeof(edid.reserved));
	edid.pad = pad;
	edid.start_block = 0;
	edid.blocks = 0;
	edid.edid = (__u8 *)0x0eadbeef;
	ret = doioctl(node, VIDIOC_G_EDID, &edid);
	if (ret == ENOTTY) {
		memset(&edid, 0, sizeof(edid));
		edid.pad = pad;
		fail_on_test(doioctl(node, VIDIOC_S_EDID, &edid) != ENOTTY);
		return ENOTTY;
	}
	has_edid = ret == 0;
	fail_on_test(ret && ret != EINVAL);
	fail_on_test(!ret && check_0(edid.reserved, sizeof(edid.reserved)));
	fail_on_test(edid.start_block);
	fail_on_test(edid.blocks > 256);
	fail_on_test(!has_edid && edid.blocks);
	fail_on_test(edid.pad != pad);
	blocks = edid.blocks;
	edid.edid = data;
	ret = doioctl(node, VIDIOC_G_EDID, &edid);
	if (!has_edid)
		fail_on_test(ret != EINVAL);
	else
		fail_on_test(ret != 0 && ret != ENODATA);
	fail_on_test(edid.blocks != blocks);

	if (has_edid) {
		edid.start_block = edid.blocks ? edid.blocks : 1;
		ret = doioctl(node, VIDIOC_G_EDID, &edid);
		fail_on_test(ret != EINVAL && ret != ENODATA);
		if (blocks > 1) {
			edid.start_block = 1;
			edid.blocks = blocks;
			fail_on_test(doioctl(node, VIDIOC_G_EDID, &edid));
			fail_on_test(edid.blocks != blocks - 1);
		}

		edid.start_block = 0;
		edid.blocks = 256;
		ret = doioctl(node, VIDIOC_G_EDID, &edid);
		fail_on_test(ret && ret != ENODATA);
		if (!ret)
			fail_on_test(edid.blocks != blocks);
		edid.pad = ~0;
		fail_on_test(doioctl(node, VIDIOC_G_EDID, &edid) != EINVAL);
		edid.pad = pad;
	}

	memset(edid.reserved, 0xff, sizeof(edid.reserved));
	edid.blocks = blocks;
	ret = doioctl(node, VIDIOC_S_EDID, &edid);
	if (ret == ENOTTY) {
		fail_on_test(is_input);
		return 0;
	}
	fail_on_test(!is_input);
	if (!has_edid) {
		fail_on_test(ret != EINVAL);
		return 0;
	}
	fail_on_test(ret);
	fail_on_test(check_0(edid.reserved, sizeof(edid.reserved)));
	edid.blocks = blocks;
	edid.pad = ~0;
	ret = doioctl(node, VIDIOC_S_EDID, &edid);
	fail_on_test(ret != EINVAL);
	if (blocks < 256) {
		edid.blocks = 256;
		edid.pad = pad;
		ret = doioctl(node, VIDIOC_S_EDID, &edid);
		fail_on_test(ret != E2BIG);
		fail_on_test(edid.blocks == 0 || edid.blocks >= 256);
		fail_on_test(edid.pad != pad);
	}
	if (!has_mmu)
		return 0;
	edid.blocks = 1;
	edid.pad = pad;
	edid.edid = nullptr;
	ret = doioctl(node, VIDIOC_S_EDID, &edid);
	fail_on_test(ret != EFAULT);
	edid.edid = (__u8 *)0x0eadbeef;
	ret = doioctl(node, VIDIOC_S_EDID, &edid);
	fail_on_test(ret != EFAULT);
	return 0;
}

int testEdid(struct node *node)
{
	bool has_edid = false;
	unsigned i, o;
	int ret;

	if (node->is_subdev()) {
		for (unsigned pad = 0; pad < node->entity.pads; pad++) {
			bool is_input = node->pads[pad].flags & MEDIA_PAD_FL_SOURCE;

			ret = checkEdid(node, pad, is_input);
			if (ret && ret != ENOTTY)
				return fail("EDID check failed for %s pad %u.\n",
					    is_input ? "source" : "sink", pad);
			if (!ret)
				has_edid = true;
		}
		return has_edid ? 0 : ENOTTY;
	}

	for (i = 0; i < node->inputs; i++) {
		struct v4l2_input input;

		input.index = i;
		ret = doioctl(node, VIDIOC_ENUMINPUT, &input);
		if (ret)
			return fail("could not enumerate input %d?!\n", i);
		ret = doioctl(node, VIDIOC_S_INPUT, &input.index);
		if (ret)
			return fail("could not select input %d.\n", i);
		ret = checkEdid(node, i, true);
		if (ret && ret != ENOTTY)
			return fail("EDID check failed for input %d.\n", i);
		if (input.capabilities & V4L2_IN_CAP_DV_TIMINGS)
			has_edid = !ret ? true : has_edid;
	}

	for (o = 0; o < node->outputs; o++) {
		struct v4l2_output output;

		output.index = o;
		ret = doioctl(node, VIDIOC_ENUMOUTPUT, &output);
		if (ret)
			return fail("could not enumerate output %d?!\n", o);
		ret = doioctl(node, VIDIOC_S_OUTPUT, &output.index);
		if (ret)
			return fail("could not select output %d.\n", o);
		ret = checkEdid(node, o, false);
		if (ret && ret != ENOTTY)
			return fail("EDID check failed for output %d.\n", o);
		if (output.capabilities & V4L2_OUT_CAP_DV_TIMINGS)
			has_edid = !ret ? true : has_edid;
	}
	return has_edid ? 0 : ENOTTY;
}
